diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java index ba689d5107f7..433e173fbbf9 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/storage/IscsiAdmStorageAdaptor.java @@ -19,6 +19,8 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.nio.file.Files; +import java.nio.file.Paths; import org.apache.cloudstack.utils.qemu.QemuImg; import org.apache.cloudstack.utils.qemu.QemuImg.PhysicalDiskFormat; @@ -96,10 +98,15 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map 0) { @@ -238,6 +267,15 @@ public KVMPhysicalDisk getPhysicalDisk(String volumeUuid, KVMStoragePool pool) { } private long getDeviceSize(String deviceByPath) { + try { + if (!Files.exists(Paths.get(deviceByPath))) { + logger.debug("Device by-path does not exist yet: " + deviceByPath); + return 0L; + } + } catch (Exception ignore) { + // If FS check fails for any reason, fall back to blockdev call + } + Script iScsiAdmCmd = new Script(true, "blockdev", 0, logger); iScsiAdmCmd.add("--getsize64", deviceByPath); diff --git a/plugins/storage/volume/ontap/README.md b/plugins/storage/volume/ontap/README.md new file mode 100644 index 000000000000..e7e066aafb55 --- /dev/null +++ b/plugins/storage/volume/ontap/README.md @@ -0,0 +1,123 @@ + + +# Apache CloudStack - NetApp ONTAP Storage Plugin + +## Overview + +The NetApp ONTAP Storage Plugin provides integration between Apache CloudStack and NetApp ONTAP storage systems. This plugin enables CloudStack to provision and manage primary storage on ONTAP clusters, supporting both NAS (NFS) and SAN (iSCSI) protocols. + +## Features + +- **Primary Storage Support**: Provision and manage primary storage pools on NetApp ONTAP +- **Multiple Protocols**: Support for NFS 3.0 and iSCSI protocols +- **Unified Storage**: Integration with traditional ONTAP unified storage architecture +- **KVM Hypervisor Support**: Supports KVM hypervisor environments +- **Managed Storage**: Operates as managed storage with full lifecycle management +- **Flexible Scoping**: Support for Zone-wide and Cluster-scoped storage pools + +## Architecture + +### Component Structure + +| Package | Description | +|---------|-------------------------------------------------------| +| `driver` | Primary datastore driver implementation | +| `feign` | REST API clients and data models for ONTAP operations | +| `lifecycle` | Storage pool lifecycle management | +| `listener` | Host connection event handlers | +| `provider` | Main provider and strategy factory | +| `service` | ONTAP Storage strategy implementations (NAS/SAN) | +| `utils` | Constants and helper utilities | + +## Requirements + +### ONTAP Requirements + +- NetApp ONTAP 9.15.1 or higher +- Storage Virtual Machine (SVM) configured with appropriate protocols enabled +- Management LIF accessible from CloudStack management server +- Data LIF(s) accessible from hypervisor hosts and are of IPv4 type +- Aggregates assigned to the SVM with sufficient capacity + +### CloudStack Requirements + +- Apache CloudStack current version or higher +- KVM hypervisor hosts +- For iSCSI: Hosts must have iSCSI initiator configured with valid IQN +- For NFS: Hosts must have NFS client packages installed + +### Minimum Volume Size + +ONTAP requires a minimum volume size of **1.56 GB** (1,677,721,600 bytes). The plugin will automatically adjust requested sizes below this threshold. + +## Configuration + +### Storage Pool Creation Parameters + +When creating an ONTAP primary storage pool, provide the following details in the URL field (semicolon-separated key=value pairs): + +| Parameter | Required | Description | +|-----------|----------|-------------| +| `username` | Yes | ONTAP cluster admin username | +| `password` | Yes | ONTAP cluster admin password | +| `svmName` | Yes | Storage Virtual Machine name | +| `protocol` | Yes | Storage protocol (`NFS3` or `ISCSI`) | +| `managementLIF` | Yes | ONTAP cluster management LIF IP address | + +### Example URL Format + +``` +username=admin;password=secretpass;svmName=svm1;protocol=ISCSI;managementLIF=192.168.1.100 +``` + +## Port Configuration + +| Protocol | Default Port | +|----------|--------------| +| NFS | 2049 | +| iSCSI | 3260 | +| ONTAP Management API | 443 (HTTPS) | + +## Limitations + +- Supports only **KVM** hypervisor +- Supports only **Unified ONTAP** storage (disaggregated not supported) +- Supports only **NFS3** and **iSCSI** protocols +- IPv6 type and FQDN LIFs are not supported + +## Troubleshooting + +### Common Issues + +1. **Connection Failures** + - Verify management LIF is reachable from CloudStack management server + - Check firewall rules for port 443 + +2. **Protocol Errors** + - Ensure the protocol (NFS/iSCSI) is enabled on the SVM + - Verify Data LIFs are configured for the protocol + +3. **Capacity Errors** + - Check aggregate space availability + - Ensure requested volume size meets minimum requirements (1.56 GB) + +4. **Host Connection Issues** + - For iSCSI: Verify host IQN is properly configured in host's storage URL + - For NFS: Ensure NFS client is installed and running diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java index 5e79aa2298da..f912449f269e 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java @@ -27,8 +27,14 @@ import com.cloud.storage.Storage; import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.ScopeType; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; +import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult; import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult; @@ -44,7 +50,11 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.storage.feign.model.Lun; +import org.apache.cloudstack.storage.service.SANStrategy; import org.apache.cloudstack.storage.service.StorageStrategy; +import org.apache.cloudstack.storage.service.UnifiedSANStrategy; +import org.apache.cloudstack.storage.service.model.AccessGroup; import org.apache.cloudstack.storage.service.model.CloudStackVolume; import org.apache.cloudstack.storage.service.model.ProtocolType; import org.apache.cloudstack.storage.utils.Constants; @@ -53,16 +63,23 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +/** + * Primary datastore driver for NetApp ONTAP storage systems. + * Handles volume lifecycle operations for iSCSI and NFS protocols. + */ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver { private static final Logger s_logger = LogManager.getLogger(OntapPrimaryDatastoreDriver.class); @Inject private StoragePoolDetailsDao storagePoolDetailsDao; @Inject private PrimaryDataStoreDao storagePoolDao; - + @Inject private VMInstanceDao vmDao; + @Inject private VolumeDao volumeDao; + @Inject private VolumeDetailsDao volumeDetailsDao; @Override public Map getCapabilities() { s_logger.trace("OntapPrimaryDatastoreDriver: getCapabilities: Called"); @@ -71,7 +88,6 @@ public Map getCapabilities() { // TODO Set it to false once we start supporting snapshot feature mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.FALSE.toString()); mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.FALSE.toString()); - return mapCapabilities; } @@ -81,13 +97,21 @@ public DataTO getTO(DataObject data) { } @Override - public DataStoreTO getStoreTO(DataStore store) { return null; } + public DataStoreTO getStoreTO(DataStore store) { + return null; + } + /** + * Creates a volume on the ONTAP storage system. + */ @Override public void createAsync(DataStore dataStore, DataObject dataObject, AsyncCompletionCallback callback) { CreateCmdResult createCmdResult = null; - String path = null; - String errMsg = null; + String errMsg; + + if (dataObject == null) { + throw new InvalidParameterValueException("createAsync: dataObject should not be null"); + } if (dataStore == null) { throw new InvalidParameterValueException("createAsync: dataStore should not be null"); } @@ -97,13 +121,71 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet if (callback == null) { throw new InvalidParameterValueException("createAsync: callback should not be null"); } + try { - s_logger.info("createAsync: Started for data store [{}] and data object [{}] of type [{}]", - dataStore, dataObject, dataObject.getType()); + s_logger.info("createAsync: Started for data store name [{}] and data object name [{}] of type [{}]", + dataStore.getName(), dataObject.getName(), dataObject.getType()); + + StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); + if (storagePool == null) { + s_logger.error("createAsync: Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("createAsync: Storage Pool not found for id: " + dataStore.getId()); + } + String storagePoolUuid = dataStore.getUuid(); + + Map details = storagePoolDetailsDao.listDetailsKeyPairs(dataStore.getId()); + if (dataObject.getType() == DataObjectType.VOLUME) { - VolumeInfo volumeInfo = (VolumeInfo) dataObject; - path = createCloudStackVolumeForTypeVolume(dataStore, volumeInfo); - createCmdResult = new CreateCmdResult(path, new Answer(null, true, null)); + VolumeInfo volInfo = (VolumeInfo) dataObject; + + // Create the backend storage object (LUN for iSCSI, no-op for NFS) + CloudStackVolume created = createCloudStackVolume(dataStore, volInfo, details); + + // Update CloudStack volume record with storage pool association and protocol-specific details + VolumeVO volumeVO = volumeDao.findById(volInfo.getId()); + if (volumeVO != null) { + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + String svmName = details.get(Constants.SVM_NAME); + String lunName = created != null && created.getLun() != null ? created.getLun().getName() : null; + if (lunName == null) { + throw new CloudRuntimeException("createAsync: Missing LUN name for volume " + volInfo.getId()); + } + + // Determine scope ID based on storage pool scope (cluster or zone level igroup) + long scopeId = (storagePool.getScope() == ScopeType.CLUSTER) + ? storagePool.getClusterId() + : storagePool.getDataCenterId(); + + // Persist LUN details for future operations (delete, grant/revoke access) + volumeDetailsDao.addDetail(volInfo.getId(), Constants.LUN_DOT_UUID, created.getLun().getUuid(), false); + volumeDetailsDao.addDetail(volInfo.getId(), Constants.LUN_DOT_NAME, lunName, false); + if (created.getLun().getUuid() != null) { + volumeVO.setFolder(created.getLun().getUuid()); + } + + // Create LUN-to-igroup mapping and retrieve the assigned LUN ID + UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) Utility.getStrategyByStoragePoolDetails(details); + String accessGroupName = Utility.getIgroupName(svmName, storagePoolUuid); + String lunNumber = sanStrategy.ensureLunMapped(svmName, lunName, accessGroupName); + + // Construct iSCSI path: // format for KVM/libvirt attachment + String iscsiPath = Constants.SLASH + storagePool.getPath() + Constants.SLASH + lunNumber; + volumeVO.set_iScsiName(iscsiPath); + volumeVO.setPath(iscsiPath); + s_logger.info("createAsync: Volume [{}] iSCSI path set to {}", volumeVO.getId(), iscsiPath); + + } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + // For NFS, the hypervisor handles file creation; we only track pool association + s_logger.info("createAsync: Managed NFS volume [{}] associated with pool {}", + volumeVO.getId(), storagePool.getId()); + } + + volumeDao.update(volumeVO.getId(), volumeVO); + } + createCmdResult = new CreateCmdResult(null, new Answer(null, true, null)); } else { errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to createAsync"; s_logger.error(errMsg); @@ -111,39 +193,41 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet } } catch (Exception e) { errMsg = e.getMessage(); - s_logger.error("createAsync: Failed for dataObject [{}]: {}", dataObject, errMsg); + s_logger.error("createAsync: Failed for dataObject name [{}]: {}", dataObject.getName(), errMsg); createCmdResult = new CreateCmdResult(null, new Answer(null, false, errMsg)); createCmdResult.setResult(e.toString()); } finally { if (createCmdResult != null && createCmdResult.isSuccess()) { - s_logger.info("createAsync: Volume created successfully. Path: {}", path); + s_logger.info("createAsync: Operation completed successfully for {}", dataObject.getType()); } callback.complete(createCmdResult); } } - private String createCloudStackVolumeForTypeVolume(DataStore dataStore, VolumeInfo volumeObject) { + /** + * Creates a volume on the ONTAP backend. + */ + private CloudStackVolume createCloudStackVolume(DataStore dataStore, DataObject dataObject, Map details) { StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); - if(storagePool == null) { - s_logger.error("createCloudStackVolume : Storage Pool not found for id: " + dataStore.getId()); - throw new CloudRuntimeException("createCloudStackVolume : Storage Pool not found for id: " + dataStore.getId()); + if (storagePool == null) { + s_logger.error("createCloudStackVolume: Storage Pool not found for id: {}", dataStore.getId()); + throw new CloudRuntimeException("createCloudStackVolume: Storage Pool not found for id: " + dataStore.getId()); } - Map details = storagePoolDetailsDao.listDetailsKeyPairs(dataStore.getId()); + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); - s_logger.info("createCloudStackVolumeForTypeVolume: Connection to Ontap SVM [{}] successful, preparing CloudStackVolumeRequest", details.get(Constants.SVM_NAME)); - CloudStackVolume cloudStackVolumeRequest = Utility.createCloudStackVolumeRequestByProtocol(storagePool, details, volumeObject); - CloudStackVolume cloudStackVolume = storageStrategy.createCloudStackVolume(cloudStackVolumeRequest); - if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL)) && cloudStackVolume.getLun() != null && cloudStackVolume.getLun().getName() != null) { - return cloudStackVolume.getLun().getName(); - } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { - return volumeObject.getUuid(); // return the volume UUID for agent as path for mounting + + if (dataObject.getType() == DataObjectType.VOLUME) { + VolumeInfo volumeObject = (VolumeInfo) dataObject; + CloudStackVolume cloudStackVolumeRequest = Utility.createCloudStackVolumeRequestByProtocol(storagePool, details, volumeObject); + return storageStrategy.createCloudStackVolume(cloudStackVolumeRequest); } else { - String errMsg = "createCloudStackVolumeForTypeVolume: Volume creation failed. Lun or Lun Path is null for dataObject: " + volumeObject; - s_logger.error(errMsg); - throw new CloudRuntimeException(errMsg); + throw new CloudRuntimeException("createCloudStackVolume: Unsupported DataObjectType: " + dataObject.getType()); } } + /** + * Deletes a volume from the ONTAP storage system. + */ @Override public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) { CommandResult commandResult = new CommandResult(); @@ -151,19 +235,50 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac if (store == null || data == null) { throw new CloudRuntimeException("deleteAsync: store or data is null"); } + if (data.getType() == DataObjectType.VOLUME) { StoragePoolVO storagePool = storagePoolDao.findById(store.getId()); - if(storagePool == null) { - s_logger.error("deleteAsync : Storage Pool not found for id: " + store.getId()); - throw new CloudRuntimeException("deleteAsync : Storage Pool not found for id: " + store.getId()); + if (storagePool == null) { + s_logger.error("deleteAsync: Storage Pool not found for id: " + store.getId()); + throw new CloudRuntimeException("deleteAsync: Storage Pool not found for id: " + store.getId()); } + Map details = storagePoolDetailsDao.listDetailsKeyPairs(store.getId()); + if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { - // ManagedNFS qcow2 backing file deletion handled by KVM host/libvirt; nothing to do via ONTAP REST. - s_logger.info("deleteAsync: ManagedNFS volume {} no-op ONTAP deletion", data.getId()); + // NFS file deletion is handled by the hypervisor; no ONTAP REST call needed + s_logger.info("deleteAsync: ManagedNFS volume {} - file deletion handled by hypervisor", data.getId()); + + } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); + VolumeInfo volumeObject = (VolumeInfo) data; + s_logger.info("deleteAsync: Deleting LUN for volume id [{}]", volumeObject.getId()); + + // Retrieve LUN identifiers stored during volume creation + String lunName = volumeDetailsDao.findDetail(volumeObject.getId(), Constants.LUN_DOT_NAME).getValue(); + String lunUUID = volumeDetailsDao.findDetail(volumeObject.getId(), Constants.LUN_DOT_UUID).getValue(); + if (lunName == null) { + throw new CloudRuntimeException("deleteAsync: Missing LUN name for volume " + volumeObject.getId()); + } + + CloudStackVolume delRequest = new CloudStackVolume(); + Lun lun = new Lun(); + lun.setName(lunName); + lun.setUuid(lunUUID); + delRequest.setLun(lun); + storageStrategy.deleteCloudStackVolume(delRequest); + + commandResult.setResult(null); + commandResult.setSuccess(true); + s_logger.info("deleteAsync: LUN [{}] deleted successfully", lunName); + + } else { + throw new CloudRuntimeException("deleteAsync: Unsupported protocol: " + details.get(Constants.PROTOCOL)); } } } catch (Exception e) { + s_logger.error("deleteAsync: Failed for data object [{}]: {}", data, e.getMessage()); + commandResult.setSuccess(false); commandResult.setResult(e.getMessage()); } finally { callback.complete(commandResult); @@ -172,7 +287,6 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac @Override public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) { - } @Override @@ -195,13 +309,228 @@ public ChapInfo getChapInfo(DataObject dataObject) { return null; } + /** + * Grants a host access to a volume. + */ @Override public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore) { - return true; + try { + if (dataStore == null) { + throw new InvalidParameterValueException("grantAccess: dataStore should not be null"); + } + if (dataObject == null) { + throw new InvalidParameterValueException("grantAccess: dataObject should not be null"); + } + if (host == null) { + throw new InvalidParameterValueException("grantAccess: host should not be null"); + } + + StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); + if (storagePool == null) { + s_logger.error("grantAccess: Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("grantAccess: Storage Pool not found for id: " + dataStore.getId()); + } + String storagePoolUuid = dataStore.getUuid(); + + // ONTAP managed storage only supports cluster and zone scoped pools + if (storagePool.getScope() != ScopeType.CLUSTER && storagePool.getScope() != ScopeType.ZONE) { + s_logger.error("grantAccess: Only Cluster and Zone scoped primary storage is supported for storage Pool: " + storagePool.getName()); + throw new CloudRuntimeException("grantAccess: Only Cluster and Zone scoped primary storage is supported for Storage Pool: " + storagePool.getName()); + } + + if (dataObject.getType() == DataObjectType.VOLUME) { + VolumeVO volumeVO = volumeDao.findById(dataObject.getId()); + if (volumeVO == null) { + s_logger.error("grantAccess: CloudStack Volume not found for id: " + dataObject.getId()); + throw new CloudRuntimeException("grantAccess: CloudStack Volume not found for id: " + dataObject.getId()); + } + + Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId()); + String svmName = details.get(Constants.SVM_NAME); + String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), Constants.LUN_DOT_NAME).getValue(); + long scopeId = (storagePool.getScope() == ScopeType.CLUSTER) ? host.getClusterId() : host.getDataCenterId(); + + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) Utility.getStrategyByStoragePoolDetails(details); + String accessGroupName = Utility.getIgroupName(svmName, storagePoolUuid); + + // Verify host initiator is registered in the igroup before allowing access + if (!sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroupName)) { + throw new CloudRuntimeException("grantAccess: Host initiator [" + host.getStorageUrl() + + "] is not present in iGroup [" + accessGroupName + "]"); + } + + // Create or retrieve existing LUN mapping + String lunNumber = sanStrategy.ensureLunMapped(svmName, cloudStackVolumeName, accessGroupName); + + // Update volume path if changed (e.g., after migration or re-mapping) + String iscsiPath = Constants.SLASH + storagePool.getPath() + Constants.SLASH + lunNumber; + if (volumeVO.getPath() == null || !volumeVO.getPath().equals(iscsiPath)) { + volumeVO.set_iScsiName(iscsiPath); + volumeVO.setPath(iscsiPath); + } + } + + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + volumeDao.update(volumeVO.getId(), volumeVO); + } else { + s_logger.error("Invalid DataObjectType (" + dataObject.getType() + ") passed to grantAccess"); + throw new CloudRuntimeException("Invalid DataObjectType (" + dataObject.getType() + ") passed to grantAccess"); + } + return true; + } catch (Exception e) { + s_logger.error("grantAccess: Failed for dataObject [{}]: {}", dataObject, e.getMessage()); + throw new CloudRuntimeException("grantAccess: Failed with error: " + e.getMessage(), e); + } } + /** + * Revokes a host's access to a volume. + */ @Override public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) { + try { + if (dataStore == null) { + throw new InvalidParameterValueException("revokeAccess: dataStore should not be null"); + } + if (dataObject == null) { + throw new InvalidParameterValueException("revokeAccess: dataObject should not be null"); + } + if (host == null) { + throw new InvalidParameterValueException("revokeAccess: host should not be null"); + } + + // Safety check: don't revoke access if volume is still attached to an active VM + if (dataObject.getType() == DataObjectType.VOLUME) { + Volume volume = volumeDao.findById(dataObject.getId()); + if (volume.getInstanceId() != null) { + VirtualMachine vm = vmDao.findById(volume.getInstanceId()); + if (vm != null && !Arrays.asList( + VirtualMachine.State.Destroyed, + VirtualMachine.State.Expunging, + VirtualMachine.State.Error).contains(vm.getState())) { + s_logger.warn("revokeAccess: Volume [{}] is still attached to VM [{}] in state [{}], skipping revokeAccess", + dataObject.getId(), vm.getInstanceName(), vm.getState()); + return; + } + } + } + + StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); + if (storagePool == null) { + s_logger.error("revokeAccess: Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("revokeAccess: Storage Pool not found for id: " + dataStore.getId()); + } + + if (storagePool.getScope() != ScopeType.CLUSTER && storagePool.getScope() != ScopeType.ZONE) { + s_logger.error("revokeAccess: Only Cluster and Zone scoped primary storage is supported for storage Pool: " + storagePool.getName()); + throw new CloudRuntimeException("revokeAccess: Only Cluster and Zone scoped primary storage is supported for Storage Pool: " + storagePool.getName()); + } + + if (dataObject.getType() == DataObjectType.VOLUME) { + VolumeVO volumeVO = volumeDao.findById(dataObject.getId()); + if (volumeVO == null) { + s_logger.error("revokeAccess: CloudStack Volume not found for id: " + dataObject.getId()); + throw new CloudRuntimeException("revokeAccess: CloudStack Volume not found for id: " + dataObject.getId()); + } + revokeAccessForVolume(storagePool, volumeVO, host); + } else { + s_logger.error("revokeAccess: Invalid DataObjectType (" + dataObject.getType() + ") passed to revokeAccess"); + throw new CloudRuntimeException("Invalid DataObjectType (" + dataObject.getType() + ") passed to revokeAccess"); + } + } catch (Exception e) { + s_logger.error("revokeAccess: Failed for dataObject [{}]: {}", dataObject, e.getMessage()); + throw new CloudRuntimeException("revokeAccess: Failed with error: " + e.getMessage(), e); + } + } + + /** + * Revokes volume access for the specified host. + */ + private void revokeAccessForVolume(StoragePoolVO storagePool, VolumeVO volumeVO, Host host) { + s_logger.info("revokeAccessForVolume: Revoking access to volume [{}] for host [{}]", volumeVO.getName(), host.getName()); + + Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId()); + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); + String svmName = details.get(Constants.SVM_NAME); + String storagePoolUuid = storagePool.getUuid(); + long scopeId = (storagePool.getScope() == ScopeType.CLUSTER) ? host.getClusterId() : host.getDataCenterId(); + + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + String accessGroupName = Utility.getIgroupName(svmName, storagePoolUuid); + + // Retrieve LUN name from volume details; if missing, volume may not have been fully created + String lunName = volumeDetailsDao.findDetail(volumeVO.getId(), Constants.LUN_DOT_NAME) != null ? + volumeDetailsDao.findDetail(volumeVO.getId(), Constants.LUN_DOT_NAME).getValue() : null; + if (lunName == null) { + s_logger.warn("revokeAccessForVolume: No LUN name found for volume [{}]; skipping revoke", volumeVO.getId()); + return; + } + + // Verify LUN still exists on ONTAP (may have been manually deleted) + CloudStackVolume cloudStackVolume = getCloudStackVolumeByName(storageStrategy, svmName, lunName); + if (cloudStackVolume == null || cloudStackVolume.getLun() == null || cloudStackVolume.getLun().getUuid() == null) { + s_logger.warn("revokeAccessForVolume: LUN for volume [{}] not found on ONTAP, skipping revoke", volumeVO.getId()); + return; + } + + // Verify igroup still exists on ONTAP + AccessGroup accessGroup = getAccessGroupByName(storageStrategy, svmName, accessGroupName); + if (accessGroup == null || accessGroup.getIgroup() == null || accessGroup.getIgroup().getUuid() == null) { + s_logger.warn("revokeAccessForVolume: iGroup [{}] not found on ONTAP, skipping revoke", accessGroupName); + return; + } + + // Verify host initiator is in the igroup before attempting to remove mapping + SANStrategy sanStrategy = (UnifiedSANStrategy) storageStrategy; + if (!sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroup.getIgroup().getName())) { + s_logger.warn("revokeAccessForVolume: Initiator [{}] is not in iGroup [{}], skipping revoke", + host.getStorageUrl(), accessGroupName); + return; + } + + // Remove the LUN mapping from the igroup + Map disableLogicalAccessMap = new HashMap<>(); + disableLogicalAccessMap.put(Constants.LUN_DOT_UUID, cloudStackVolume.getLun().getUuid()); + disableLogicalAccessMap.put(Constants.IGROUP_DOT_UUID, accessGroup.getIgroup().getUuid()); + storageStrategy.disableLogicalAccess(disableLogicalAccessMap); + + s_logger.info("revokeAccessForVolume: Successfully revoked access to LUN [{}] for host [{}]", + lunName, host.getName()); + } + } + + /** + * Retrieves a volume from ONTAP by name. + */ + private CloudStackVolume getCloudStackVolumeByName(StorageStrategy storageStrategy, String svmName, String cloudStackVolumeName) { + Map getCloudStackVolumeMap = new HashMap<>(); + getCloudStackVolumeMap.put(Constants.NAME, cloudStackVolumeName); + getCloudStackVolumeMap.put(Constants.SVM_DOT_NAME, svmName); + + CloudStackVolume cloudStackVolume = storageStrategy.getCloudStackVolume(getCloudStackVolumeMap); + if (cloudStackVolume == null || cloudStackVolume.getLun() == null || cloudStackVolume.getLun().getName() == null) { + s_logger.warn("getCloudStackVolumeByName: LUN [{}] not found on ONTAP", cloudStackVolumeName); + return null; + } + return cloudStackVolume; + } + + /** + * Retrieves an access group from ONTAP by name. + */ + private AccessGroup getAccessGroupByName(StorageStrategy storageStrategy, String svmName, String accessGroupName) { + Map getAccessGroupMap = new HashMap<>(); + getAccessGroupMap.put(Constants.NAME, accessGroupName); + getAccessGroupMap.put(Constants.SVM_DOT_NAME, svmName); + + AccessGroup accessGroup = storageStrategy.getAccessGroup(getAccessGroupMap); + if (accessGroup == null || accessGroup.getIgroup() == null || accessGroup.getIgroup().getName() == null) { + s_logger.warn("getAccessGroupByName: iGroup [{}] not found on ONTAP", accessGroupName); + return null; + } + return accessGroup; } @Override diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java index 868aab293518..45a20fe876fe 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java @@ -53,9 +53,9 @@ public interface SANFeignClient { @Headers({"Authorization: {authHeader}"}) void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun); - @RequestLine("DELETE /{uuid}") + @RequestLine("DELETE /api/storage/luns/{uuid}") @Headers({"Authorization: {authHeader}"}) - void deleteLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid); + void deleteLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, @QueryMap Map queryMap); // iGroup Operation APIs @RequestLine("POST /api/protocols/san/igroups?return_records={returnRecords}") diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Igroup.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Igroup.java index 877d60de830c..4dc07e349fad 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Igroup.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Igroup.java @@ -48,7 +48,7 @@ public class Igroup { private String name = null; @JsonProperty("protocol") - private ProtocolEnum protocol = ProtocolEnum.mixed; + private ProtocolEnum protocol = null; @JsonProperty("svm") private Svm svm = null; @JsonProperty("uuid") diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java index 48ebc9c739cb..364790958c8a 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java @@ -83,6 +83,9 @@ public static PropertyClassEnum fromValue(String value) { @JsonProperty("name") private String name = null; + @JsonProperty("clone") + private Clone clone = null; + /** * The operating system type of the LUN.<br/> Required in POST when creating a LUN that is not a clone of another. Disallowed in POST when creating a LUN clone. */ @@ -249,6 +252,14 @@ public void setUuid(String uuid) { this.uuid = uuid; } + public Clone getClone() { + return clone; + } + + public void setClone(Clone clone) { + this.clone = clone; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -295,4 +306,36 @@ private String toIndentedString(Object o) { } return o.toString().replace("\n", "\n "); } + + + public static class Clone { + @JsonProperty("source") + private Source source = null; + public Source getSource() { + return source; + } + public void setSource(Source source) { + this.source = source; + } + } + + public static class Source { + @JsonProperty("name") + private String name = null; + @JsonProperty("uuid") + private String uuid = null; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public String getUuid() { + return uuid; + } + public void setUuid(String uuid) { + this.uuid = uuid; + } + } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Svm.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Svm.java index 65821739f1b2..b1462c593863 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Svm.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Svm.java @@ -143,5 +143,4 @@ public int hashCode() { @JsonInclude(JsonInclude.Include.NON_NULL) public static class Links { } - } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java index 2cdd7de0b7c5..a7df490b9b95 100755 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycle.java @@ -23,6 +23,7 @@ import com.cloud.agent.api.StoragePoolInfo; import com.cloud.dc.ClusterVO; import com.cloud.dc.dao.ClusterDao; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.resource.ResourceManager; @@ -42,8 +43,8 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDetailsDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.datastore.lifecycle.BasePrimaryDataStoreLifeCycleImpl; -import org.apache.cloudstack.storage.feign.model.ExportPolicy; import org.apache.cloudstack.storage.feign.model.OntapStorage; import org.apache.cloudstack.storage.feign.model.Volume; import org.apache.cloudstack.storage.provider.StorageProviderFactory; @@ -57,6 +58,7 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @@ -67,10 +69,10 @@ public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycl @Inject private StorageManager _storageMgr; @Inject private ResourceManager _resourceMgr; @Inject private PrimaryDataStoreHelper _dataStoreHelper; - @Inject private PrimaryDataStoreDao storagePoolDao; - @Inject private StoragePoolDetailsDao storagePoolDetailsDao; @Inject private PrimaryDataStoreDetailsDao _datastoreDetailsDao; @Inject private StoragePoolAutomation _storagePoolAutomation; + @Inject private PrimaryDataStoreDao storagePoolDao; + @Inject private StoragePoolDetailsDao storagePoolDetailsDao; private static final Logger s_logger = LogManager.getLogger(OntapPrimaryDatastoreLifecycle.class); // ONTAP minimum volume size is 1.56 GB (1677721600 bytes) @@ -150,16 +152,21 @@ public DataStore initialize(Map dsInfos) { throw new CloudRuntimeException("ONTAP primary storage must be managed"); } - // Required ONTAP detail keys Set requiredKeys = Set.of( Constants.USERNAME, Constants.PASSWORD, Constants.SVM_NAME, Constants.PROTOCOL, - Constants.MANAGEMENT_LIF, + Constants.MANAGEMENT_LIF + ); + + Set optionalKeys = Set.of( Constants.IS_DISAGGREGATED ); + Set allowedKeys = new java.util.HashSet<>(requiredKeys); + allowedKeys.addAll(optionalKeys); + // Parse key=value pairs from URL into details (skip empty segments) if (url != null && !url.isEmpty()) { for (String segment : url.split(Constants.SEMICOLON)) { @@ -177,7 +184,7 @@ public DataStore initialize(Map dsInfos) { for (Map.Entry e : details.entrySet()) { String key = e.getKey(); String val = e.getValue(); - if (!requiredKeys.contains(key)) { + if (!allowedKeys.contains(key)) { throw new CloudRuntimeException("Unexpected ONTAP detail key in URL: " + key); } if (val == null || val.isEmpty()) { @@ -247,13 +254,13 @@ public DataStore initialize(Map dsInfos) { case NFS3: parameters.setType(Storage.StoragePoolType.NetworkFilesystem); path = Constants.SLASH + storagePoolName; - port = 2049; + port = Constants.NFS3_PORT; s_logger.info("Setting NFS path for storage pool: " + path + ", port: " + port); break; case ISCSI: parameters.setType(Storage.StoragePoolType.Iscsi); path = storageStrategy.getStoragePath(); - port = 3260; + port = Constants.ISCSI_PORT; s_logger.info("Setting iSCSI path for storage pool: " + path + ", port: " + port); break; default: @@ -283,30 +290,55 @@ public DataStore initialize(Map dsInfos) { @Override public boolean attachCluster(DataStore dataStore, ClusterScope scope) { logger.debug("In attachCluster for ONTAP primary storage"); + if (dataStore == null) { + throw new InvalidParameterValueException("attachCluster: dataStore should not be null"); + } + if (scope == null) { + throw new InvalidParameterValueException("attachCluster: scope should not be null"); + } + List hostsIdentifier = new ArrayList<>(); + StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); + if (storagePool == null) { + s_logger.error("attachCluster : Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("attachCluster : Storage Pool not found for id: " + dataStore.getId()); + } PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore; List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInClusterForStorageConnection(primaryStore); - - logger.debug(" datastore object received is {} ",primaryStore ); - - logger.debug(String.format("Attaching the pool to each of the hosts %s in the cluster: %s", hostsToConnect, primaryStore.getClusterId())); + // TODO- need to check if no host to connect then throw exception or just continue? + logger.debug("attachCluster: Eligible Up and Enabled hosts: {} in cluster {}", hostsToConnect, primaryStore.getClusterId()); Map details = storagePoolDetailsDao.listDetailsKeyPairs(primaryStore.getId()); StorageStrategy strategy = Utility.getStrategyByStoragePoolDetails(details); - ExportPolicy exportPolicy = new ExportPolicy(); - AccessGroup accessGroupRequest = new AccessGroup(); - accessGroupRequest.setHostsToConnect(hostsToConnect); - accessGroupRequest.setScope(scope); - primaryStore.setDetails(details);// setting details as it does not come from cloudstack - accessGroupRequest.setPrimaryDataStoreInfo(primaryStore); - accessGroupRequest.setPolicy(exportPolicy); - strategy.createAccessGroup(accessGroupRequest); + ProtocolType protocol = ProtocolType.valueOf(details.get(Constants.PROTOCOL)); + //TODO- Check if we have to handle heterogeneous host within the cluster + if (!validateProtocolSupportAndFetchHostsIdentifier(hostsToConnect, protocol, hostsIdentifier)) { + String errMsg = "attachCluster: Not all hosts in the cluster support the protocol: " + protocol.name(); + s_logger.error(errMsg); + throw new CloudRuntimeException(errMsg); + } + + logger.debug("attachCluster: Attaching the pool to each of the host in the cluster: {}", primaryStore.getClusterId()); + //TODO - check if no host to connect then also need to create access group without initiators + if (hostsIdentifier != null && hostsIdentifier.size() > 0) { + try { + AccessGroup accessGroupRequest = new AccessGroup(); + accessGroupRequest.setHostsToConnect(hostsToConnect); + accessGroupRequest.setScope(scope); + primaryStore.setDetails(details);// setting details as it does not come from cloudstack + accessGroupRequest.setPrimaryDataStoreInfo(primaryStore); + strategy.createAccessGroup(accessGroupRequest); + } catch (Exception e) { + s_logger.error("attachCluster: Failed to create access group on storage system for cluster: " + primaryStore.getClusterId() + ". Exception: " + e.getMessage()); + throw new CloudRuntimeException("attachCluster: Failed to create access group on storage system for cluster: " + primaryStore.getClusterId() + ". Exception: " + e.getMessage()); + } + } logger.debug("attachCluster: Attaching the pool to each of the host in the cluster: {}", primaryStore.getClusterId()); for (HostVO host : hostsToConnect) { try { _storageMgr.connectHostToSharedPool(host, dataStore.getId()); } catch (Exception e) { - logger.warn("Unable to establish a connection between " + host + " and " + dataStore, e); + logger.warn("attachCluster: Unable to establish a connection between " + host + " and " + dataStore, e); return false; } } @@ -322,6 +354,18 @@ public boolean attachHost(DataStore store, HostScope scope, StoragePoolInfo exis @Override public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.HypervisorType hypervisorType) { logger.debug("In attachZone for ONTAP primary storage"); + if (dataStore == null) { + throw new InvalidParameterValueException("attachZone: dataStore should not be null"); + } + if (scope == null) { + throw new InvalidParameterValueException("attachZone: scope should not be null"); + } + List hostsIdentifier = new ArrayList<>(); + StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); + if (storagePool == null) { + s_logger.error("attachZone : Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("attachZone : Storage Pool not found for id: " + dataStore.getId()); + } PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore; List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore, scope.getScopeId(), Hypervisor.HypervisorType.KVM); @@ -329,15 +373,29 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper Map details = storagePoolDetailsDao.listDetailsKeyPairs(primaryStore.getId()); StorageStrategy strategy = Utility.getStrategyByStoragePoolDetails(details); - ExportPolicy exportPolicy = new ExportPolicy(); - AccessGroup accessGroupRequest = new AccessGroup(); - accessGroupRequest.setHostsToConnect(hostsToConnect); - accessGroupRequest.setScope(scope); - primaryStore.setDetails(details); // setting details as it does not come from cloudstack - accessGroupRequest.setPrimaryDataStoreInfo(primaryStore); - accessGroupRequest.setPolicy(exportPolicy); - strategy.createAccessGroup(accessGroupRequest); + // TODO- need to check if no host to connect then throw exception or just continue + logger.debug("attachZone: Eligible Up and Enabled hosts: {}", hostsToConnect); + ProtocolType protocol = ProtocolType.valueOf(details.get(Constants.PROTOCOL)); + //TODO- Check if we have to handle heterogeneous host within the zone + if (!validateProtocolSupportAndFetchHostsIdentifier(hostsToConnect, protocol, hostsIdentifier)) { + String errMsg = "attachZone: Not all hosts in the zone support the protocol: " + protocol.name(); + s_logger.error(errMsg); + throw new CloudRuntimeException(errMsg); + } + if (hostsIdentifier != null && !hostsIdentifier.isEmpty()) { + try { + AccessGroup accessGroupRequest = new AccessGroup(); + accessGroupRequest.setHostsToConnect(hostsToConnect); + accessGroupRequest.setScope(scope); + primaryStore.setDetails(details); // setting details as it does not come from cloudstack + accessGroupRequest.setPrimaryDataStoreInfo(primaryStore); + strategy.createAccessGroup(accessGroupRequest); + } catch (Exception e) { + s_logger.error("attachZone: Failed to create access group on storage system for zone with Exception: " + e.getMessage()); + throw new CloudRuntimeException("attachZone: Failed to create access group on storage system for zone with Exception: " + e.getMessage()); + } + } for (HostVO host : hostsToConnect) { try { _storageMgr.connectHostToSharedPool(host, dataStore.getId()); @@ -350,16 +408,57 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper return true; } + private boolean validateProtocolSupportAndFetchHostsIdentifier(List hosts, ProtocolType protocolType, List hostIdentifiers) { + switch (protocolType) { + case ISCSI: + String protocolPrefix = Constants.IQN; + for (HostVO host : hosts) { + if (host == null || host.getStorageUrl() == null || host.getStorageUrl().trim().isEmpty() + || !host.getStorageUrl().startsWith(protocolPrefix)) { + return false; + } + hostIdentifiers.add(host.getStorageUrl()); + } + break; + case NFS3: + String ip = ""; + for (HostVO host : hosts) { + if (host != null) { + ip = host.getStorageIpAddress() != null ? host.getStorageIpAddress().trim() : ""; + if (ip.isEmpty() && host.getPrivateIpAddress() != null || host.getPrivateIpAddress().trim().isEmpty()) { + return false; + } else { + ip = ip.isEmpty() ? host.getPrivateIpAddress().trim() : ip; + } + } + hostIdentifiers.add(ip); + } + break; + default: + throw new CloudRuntimeException("validateProtocolSupportAndFetchHostsIdentifier : Unsupported protocol: " + protocolType.name()); + } + logger.info("validateProtocolSupportAndFetchHostsIdentifier: All hosts support the protocol: " + protocolType.name()); + return true; + } + @Override public boolean maintain(DataStore store) { - _storagePoolAutomation.maintain(store); - return _dataStoreHelper.maintain(store); + logger.info("Placing storage pool {} in maintenance mode", store); + if (_storagePoolAutomation.maintain(store)) { + return _dataStoreHelper.maintain(store); + } else { + return false; + } } @Override public boolean cancelMaintain(DataStore store) { - _storagePoolAutomation.cancelMaintain(store); - return _dataStoreHelper.cancelMaintain(store); + logger.info("Cancelling storage pool maintenance for {}", store); + if (_dataStoreHelper.cancelMaintain(store)) { + return _storagePoolAutomation.cancelMaintain(store); + } else { + return false; + } } @Override @@ -391,13 +490,27 @@ public boolean deleteDataStore(DataStore store) { PrimaryDataStoreInfo primaryDataStoreInfo = (PrimaryDataStoreInfo) store; primaryDataStoreInfo.setDetails(details); - // Create AccessGroup object with PrimaryDataStoreInfo + // Call deleteStorageVolume to delete the underlying ONTAP volume + s_logger.info("deleteDataStore: Deleting ONTAP volume for storage pool '{}'", storagePool.getName()); + Volume volume = new Volume(); + volume.setUuid(details.get(Constants.VOLUME_UUID)); + volume.setName(details.get(Constants.VOLUME_NAME)); + try { + if (volume.getUuid() == null || volume.getUuid().isEmpty() || volume.getName() == null || volume.getName().isEmpty()) { + s_logger.error("deleteDataStore: Volume UUID/Name not found in details for storage pool id: {}, cannot delete volume", storagePoolId); + throw new CloudRuntimeException("Volume UUID/Name not found in details, cannot delete ONTAP volume"); + } + storageStrategy.deleteStorageVolume(volume); + s_logger.info("deleteDataStore: Successfully deleted ONTAP volume '{}' (UUID: {}) for storage pool '{}'", + volume.getName(), volume.getUuid(), storagePool.getName()); + } catch (Exception e) { + s_logger.error("deleteDataStore: Exception while retrieving volume UUID for storage pool id: {}. Error: {}", + storagePoolId, e.getMessage(), e); + } AccessGroup accessGroup = new AccessGroup(); accessGroup.setPrimaryDataStoreInfo(primaryDataStoreInfo); - - // Call deleteAccessGroup - it will figure out scope, protocol, and all details internally + // Delete access groups associated with this storage pool storageStrategy.deleteAccessGroup(accessGroup); - s_logger.info("deleteDataStore: Successfully deleted access groups for storage pool '{}'", storagePool.getName()); } catch (Exception e) { @@ -411,7 +524,6 @@ public boolean deleteDataStore(DataStore store) { return _dataStoreHelper.deletePrimaryDataStore(store); } - @Override public boolean migrateToObjectStore(DataStore store) { return true; diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProvider.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProvider.java index 91bfd0a8584c..5b44c951a5fa 100755 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProvider.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProvider.java @@ -28,6 +28,7 @@ import org.apache.cloudstack.storage.driver.OntapPrimaryDatastoreDriver; import org.apache.cloudstack.storage.lifecycle.OntapPrimaryDatastoreLifecycle; import org.apache.cloudstack.storage.listener.OntapHostListener; +import org.apache.cloudstack.storage.utils.Constants; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @@ -65,7 +66,7 @@ public HypervisorHostListener getHostListener() { @Override public String getName() { s_logger.trace("OntapPrimaryDatastoreProvider: getName: Called"); - return "ONTAP Primary Datastore Provider"; + return Constants.ONTAP_PLUGIN_NAME; } @Override diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java index ce3b2806ef75..6be5ecfaf3f2 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/SANStrategy.java @@ -26,4 +26,25 @@ public SANStrategy(OntapStorage ontapStorage) { super(ontapStorage); } + /** + * Ensures the LUN is mapped to the specified access group (igroup). + * If a mapping already exists, returns the existing LUN number. + * If not, creates a new mapping and returns the assigned LUN number. + * + * @param svmName the SVM name + * @param lunName the LUN name + * @param accessGroupName the igroup name + * @return the logical unit number as a String + */ + public abstract String ensureLunMapped(String svmName, String lunName, String accessGroupName); + + /** + * Validates that the host initiator is present in the access group (igroup). + * + * @param hostInitiator the host initiator IQN + * @param svmName the SVM name + * @param accessGroupName the igroup name + * @return true if the initiator is found in the igroup, false otherwise + */ + public abstract boolean validateInitiatorInAccessGroup(String hostInitiator, String svmName, String accessGroupName); } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java index 822e09851f39..565cf0399663 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java @@ -108,33 +108,33 @@ public boolean connect() { if (svms != null && svms.getRecords() != null && !svms.getRecords().isEmpty()) { svm = svms.getRecords().get(0); } else { - throw new CloudRuntimeException("No SVM found on the ONTAP cluster by the name" + svmName + "."); + s_logger.error("No SVM found on the ONTAP cluster by the name" + svmName + "."); + return false; } // Validations s_logger.info("Validating SVM state and protocol settings..."); if (!Objects.equals(svm.getState(), Constants.RUNNING)) { s_logger.error("SVM " + svmName + " is not in running state."); - throw new CloudRuntimeException("SVM " + svmName + " is not in running state."); + return false; } if (Objects.equals(storage.getProtocol(), Constants.NFS) && !svm.getNfsEnabled()) { s_logger.error("NFS protocol is not enabled on SVM " + svmName); - throw new CloudRuntimeException("NFS protocol is not enabled on SVM " + svmName); + return false; } else if (Objects.equals(storage.getProtocol(), Constants.ISCSI) && !svm.getIscsiEnabled()) { s_logger.error("iSCSI protocol is not enabled on SVM " + svmName); - throw new CloudRuntimeException("iSCSI protocol is not enabled on SVM " + svmName); + return false; } - List aggrs = svm.getAggregates(); if (aggrs == null || aggrs.isEmpty()) { s_logger.error("No aggregates are assigned to SVM " + svmName); - throw new CloudRuntimeException("No aggregates are assigned to SVM " + svmName); + return false; } - this.aggregates = aggrs; s_logger.info("Successfully connected to ONTAP cluster and validated ONTAP details provided"); } catch (Exception e) { - throw new CloudRuntimeException("Failed to connect to ONTAP cluster: " + e.getMessage(), e); + s_logger.error("Failed to connect to ONTAP cluster: " + e.getMessage(), e); + return false; } return true; } @@ -147,7 +147,7 @@ public boolean connect() { * throw exception in case of disaggregated ONTAP storage * * @param volumeName the name of the volume to create - * @param size the size of the volume in bytes + * @param size the size of the volume in bytes * @return the created Volume object */ public Volume createStorageVolume(String volumeName, Long size) { @@ -425,8 +425,19 @@ public String getNetworkInterface() { OntapResponse response = networkFeignClient.getNetworkIpInterfaces(authHeader, queryParams); if (response != null && response.getRecords() != null && !response.getRecords().isEmpty()) { - // For simplicity, return the first interface's name - IpInterface ipInterface = response.getRecords().get(0); + IpInterface ipInterface = null; + // For simplicity, return the first interface's name (Of IPv4 type for NFS3) + if (storage.getProtocol() == ProtocolType.ISCSI) { + ipInterface = response.getRecords().get(0); + } else if (storage.getProtocol() == ProtocolType.NFS3) { + for (IpInterface iface : response.getRecords()) { + if (iface.getIp().getAddress().contains(".")) { + ipInterface = iface; + break; + } + } + } + s_logger.info("Retrieved network interface: " + ipInterface.getIp().getAddress()); return ipInterface.getIp().getAddress(); } else { @@ -472,26 +483,34 @@ public String getNetworkInterface() { * * @param cloudstackVolume the CloudStack volume to delete */ - abstract void deleteCloudStackVolume(CloudStackVolume cloudstackVolume); + abstract public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume); /** * Method encapsulates the behavior based on the opted protocol in subclasses. * it is going to mimic - * getLun for iSCSI, FC protocols - * getFile for NFS3.0 and NFS4.1 protocols - * getNameSpace for Nvme/TCP and Nvme/FC protocol - * - * @param cloudstackVolume the CloudStack volume to retrieve + * cloneLun for iSCSI, FC protocols + * cloneFile for NFS3.0 and NFS4.1 protocols + * cloneNameSpace for Nvme/TCP and Nvme/FC protocol + * @param cloudstackVolume the CloudStack volume to copy + */ + abstract public void copyCloudStackVolume(CloudStackVolume cloudstackVolume); + + /** + * Method encapsulates the behavior based on the opted protocol in subclasses. + * it is going to mimic + * getLun for iSCSI, FC protocols + * getFile for NFS3.0 and NFS4.1 protocols + * getNameSpace for Nvme/TCP and Nvme/FC protocol + * @param cloudStackVolumeMap the CloudStack volume to retrieve * @return the retrieved CloudStackVolume object */ - abstract CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume); + abstract public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap); /** * Method encapsulates the behavior based on the opted protocol in subclasses - * createiGroup for iSCSI and FC protocols - * createExportPolicy for NFS 3.0 and NFS 4.1 protocols - * createSubsystem for Nvme/TCP and Nvme/FC protocols - * + * createiGroup for iSCSI and FC protocols + * createExportPolicy for NFS 3.0 and NFS 4.1 protocols + * createSubsystem for Nvme/TCP and Nvme/FC protocols * @param accessGroup the access group to create * @return the created AccessGroup object */ @@ -499,20 +518,18 @@ public String getNetworkInterface() { /** * Method encapsulates the behavior based on the opted protocol in subclasses - * deleteiGroup for iSCSI and FC protocols - * deleteExportPolicy for NFS 3.0 and NFS 4.1 protocols - * deleteSubsystem for Nvme/TCP and Nvme/FC protocols - * + * deleteiGroup for iSCSI and FC protocols + * deleteExportPolicy for NFS 3.0 and NFS 4.1 protocols + * deleteSubsystem for Nvme/TCP and Nvme/FC protocols * @param accessGroup the access group to delete */ abstract public void deleteAccessGroup(AccessGroup accessGroup); /** * Method encapsulates the behavior based on the opted protocol in subclasses - * updateiGroup example add/remove-Iqn for iSCSI and FC protocols - * updateExportPolicy example add/remove-Rule for NFS 3.0 and NFS 4.1 protocols - * //TODO for Nvme/TCP and Nvme/FC protocols - * + * updateiGroup example add/remove-Iqn for iSCSI and FC protocols + * updateExportPolicy example add/remove-Rule for NFS 3.0 and NFS 4.1 protocols + * //TODO for Nvme/TCP and Nvme/FC protocols * @param accessGroup the access group to update * @return the updated AccessGroup object */ @@ -520,32 +537,37 @@ public String getNetworkInterface() { /** * Method encapsulates the behavior based on the opted protocol in subclasses - * getiGroup for iSCSI and FC protocols - * getExportPolicy for NFS 3.0 and NFS 4.1 protocols - * getNameSpace for Nvme/TCP and Nvme/FC protocols - * - * @param accessGroup the access group to retrieve - * @return the retrieved AccessGroup object + * e.g., getIGroup for iSCSI and FC protocols + * e.g., getExportPolicy for NFS 3.0 and NFS 4.1 protocols + * //TODO for Nvme/TCP and Nvme/FC protocols + * @param values map to get access group values like name, svm name etc. */ - abstract AccessGroup getAccessGroup(AccessGroup accessGroup); + abstract public AccessGroup getAccessGroup(Map values); /** * Method encapsulates the behavior based on the opted protocol in subclasses - * lunMap for iSCSI and FC protocols - * //TODO for Nvme/TCP and Nvme/FC protocols - * - * @param values + * lunMap for iSCSI and FC protocols + * //TODO for NFS 3.0 and NFS 4.1 protocols (e.g., export rule management) + * //TODO for Nvme/TCP and Nvme/FC protocols + * @param values map including SVM name, LUN name, and igroup name (for SAN) or equivalent for NAS + * @return map containing logical unit number for the new/existing mapping (SAN) or relevant info for NAS */ - abstract void enableLogicalAccess(Map values); + abstract public Map enableLogicalAccess(Map values); /** * Method encapsulates the behavior based on the opted protocol in subclasses - * lunUnmap for iSCSI and FC protocols - * //TODO for Nvme/TCP and Nvme/FC protocols - * - * @param values + * lunUnmap for iSCSI and FC protocols + * @param values map including LUN UUID and iGroup UUID (for SAN) or equivalent for NAS + */ + abstract public void disableLogicalAccess(Map values); + + /** + * Method encapsulates the behavior based on the opted protocol in subclasses + * lunMap lookup for iSCSI/FC protocols (GET-only, no side-effects) + * @param values map with SVM name, LUN name, and igroup name (for SAN) or equivalent for NAS + * @return map containing logical unit number if mapping exists; otherwise null */ - abstract void disableLogicalAccess(Map values); + abstract public Map getLogicalAccess(Map values); private Boolean jobPollForSuccess(String jobUUID) { //Create URI for GET Job API diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java index b35bedf2ef3c..9657edc614fa 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java @@ -110,12 +110,17 @@ CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { + public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { //TODO } @Override - CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume) { + public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) { + + } + + @Override + public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap) { //TODO return null; } @@ -188,21 +193,28 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) { } @Override - public AccessGroup getAccessGroup(AccessGroup accessGroup) { + public AccessGroup getAccessGroup(Map values) { //TODO return null; } @Override - void enableLogicalAccess(Map values) { + public Map enableLogicalAccess(Map values) { //TODO + return null; } @Override - void disableLogicalAccess(Map values) { + public void disableLogicalAccess(Map values) { //TODO } + @Override + public Map getLogicalAccess(Map values) { + // NAS does not use LUN mapping; nothing to fetch + return null; + } + private ExportPolicy createExportPolicy(String svmName, ExportPolicy policy) { s_logger.info("Creating export policy: {} for SVM: {}", policy, svmName); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java index 7b5372c69bdd..cab598702ecb 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java @@ -22,14 +22,16 @@ import com.cloud.host.HostVO; import com.cloud.hypervisor.Hypervisor; import com.cloud.utils.exception.CloudRuntimeException; +import feign.FeignException; import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.storage.feign.FeignClientFactory; import org.apache.cloudstack.storage.feign.client.SANFeignClient; import org.apache.cloudstack.storage.feign.model.Igroup; import org.apache.cloudstack.storage.feign.model.Initiator; -import org.apache.cloudstack.storage.feign.model.Lun; -import org.apache.cloudstack.storage.feign.model.OntapStorage; import org.apache.cloudstack.storage.feign.model.Svm; +import org.apache.cloudstack.storage.feign.model.OntapStorage; +import org.apache.cloudstack.storage.feign.model.Lun; +import org.apache.cloudstack.storage.feign.model.LunMap; import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import org.apache.cloudstack.storage.service.model.AccessGroup; import org.apache.cloudstack.storage.service.model.CloudStackVolume; @@ -86,8 +88,12 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume CloudStackVolume createdCloudStackVolume = new CloudStackVolume(); createdCloudStackVolume.setLun(lun); return createdCloudStackVolume; + } catch (FeignException e) { + s_logger.error("FeignException occurred while creating LUN: {}, Status: {}, Exception: {}", + cloudstackVolume.getLun().getName(), e.status(), e.getMessage()); + throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage()); } catch (Exception e) { - s_logger.error("Exception occurred while creating LUN: {}. Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage()); + s_logger.error("Exception occurred while creating LUN: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage()); throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage()); } } @@ -99,22 +105,111 @@ CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { - //TODO + public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { + if (cloudstackVolume == null || cloudstackVolume.getLun() == null) { + s_logger.error("deleteCloudStackVolume: Lun deletion failed. Invalid request: {}", cloudstackVolume); + throw new CloudRuntimeException("deleteCloudStackVolume : Failed to delete Lun, invalid request"); + } + s_logger.info("deleteCloudStackVolume : Deleting Lun: {}", cloudstackVolume.getLun().getName()); + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + Map queryParams = Map.of("allow_delete_while_mapped", "true"); + try { + sanFeignClient.deleteLun(authHeader, cloudstackVolume.getLun().getUuid(), queryParams); + } catch (FeignException feignEx) { + if (feignEx.status() == 404) { + s_logger.warn("deleteCloudStackVolume: Lun {} does not exist (status 404), skipping deletion", cloudstackVolume.getLun().getName()); + return; + } + throw feignEx; + } + s_logger.info("deleteCloudStackVolume: Lun deleted successfully. LunName: {}", cloudstackVolume.getLun().getName()); + } catch (Exception e) { + s_logger.error("Exception occurred while deleting Lun: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage()); + throw new CloudRuntimeException("Failed to delete Lun: " + e.getMessage()); + } } @Override - CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume) { - //TODO - return null; + public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) { + s_logger.debug("copyCloudStackVolume: Creating clone of the cloudstack volume: {}", cloudstackVolume.getLun().getName()); + if (cloudstackVolume == null || cloudstackVolume.getLun() == null) { + s_logger.error("copyCloudStackVolume: Lun clone creation failed. Invalid request: {}", cloudstackVolume); + throw new CloudRuntimeException("copyCloudStackVolume : Failed to create Lun clone, invalid request"); + } + + try { + // Get AuthHeader + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + // Create URI for lun clone creation + Lun lunCloneRequest = cloudstackVolume.getLun(); + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(cloudstackVolume.getLun().getName()); + clone.setSource(source); + lunCloneRequest.setClone(clone); + String lunCloneName = cloudstackVolume.getLun().getName() + "_clone"; + lunCloneRequest.setName(lunCloneName); + sanFeignClient.createLun(authHeader, true, lunCloneRequest); + } catch (FeignException e) { + s_logger.error("FeignException occurred while creating Lun clone: {}, Status: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.status(), e.getMessage()); + throw new CloudRuntimeException("Failed to create Lun clone: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception occurred while creating Lun clone: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage()); + throw new CloudRuntimeException("Failed to create Lun clone: " + e.getMessage()); + } + } + + @Override + public CloudStackVolume getCloudStackVolume(Map values) { + s_logger.info("getCloudStackVolume : fetching Lun"); + s_logger.debug("getCloudStackVolume : fetching Lun with params {} ", values); + if (values == null || values.isEmpty()) { + s_logger.error("getCloudStackVolume: get Lun failed. Invalid request: {}", values); + throw new CloudRuntimeException("getCloudStackVolume : get Lun Failed, invalid request"); + } + String svmName = values.get(Constants.SVM_DOT_NAME); + String lunName = values.get(Constants.NAME); + if (svmName == null || lunName == null || svmName.isEmpty() || lunName.isEmpty()) { + s_logger.error("getCloudStackVolume: get Lun failed. Invalid svm:{} or Lun name: {}", svmName, lunName); + throw new CloudRuntimeException("getCloudStackVolume : Failed to get Lun, invalid request"); + } + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + Map queryParams = Map.of(Constants.SVM_DOT_NAME, svmName, Constants.NAME, lunName); + OntapResponse lunResponse = sanFeignClient.getLunResponse(authHeader, queryParams); + if (lunResponse == null || lunResponse.getRecords() == null || lunResponse.getRecords().isEmpty()) { + s_logger.warn("getCloudStackVolume: Lun '{}' on SVM '{}' not found. Returning null.", lunName, svmName); + return null; + } + Lun lun = lunResponse.getRecords().get(0); + s_logger.debug("getCloudStackVolume: Lun Details : {}", lun); + s_logger.info("getCloudStackVolume: Fetched the Lun successfully. LunName: {}", lun.getName()); + + CloudStackVolume cloudStackVolume = new CloudStackVolume(); + cloudStackVolume.setLun(lun); + return cloudStackVolume; + } catch (FeignException e) { + if (e.status() == 404) { + s_logger.warn("getCloudStackVolume: Lun '{}' on SVM '{}' not found (status 404). Returning null.", lunName, svmName); + return null; + } + s_logger.error("FeignException occurred while fetching Lun, Status: {}, Exception: {}", e.status(), e.getMessage()); + throw new CloudRuntimeException("Failed to fetch Lun details: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception occurred while fetching Lun, Exception: {}", e.getMessage()); + throw new CloudRuntimeException("Failed to fetch Lun details: " + e.getMessage()); + } } @Override public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.info("createAccessGroup : Create Igroup"); String igroupName = "unknown"; + s_logger.debug("createAccessGroup : Creating Igroup with access group request {} ", accessGroup); if (accessGroup == null) { - throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, invalid accessGroup object passed"); + s_logger.error("createAccessGroup: Igroup creation failed. Invalid request: {}", accessGroup); + throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, invalid request"); } try { // Get StoragePool details @@ -132,7 +227,8 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { Igroup igroupRequest = new Igroup(); List hostsIdentifier = new ArrayList<>(); String svmName = dataStoreDetails.get(Constants.SVM_NAME); - igroupName = Utility.getIgroupName(svmName, accessGroup.getScope().getScopeType(), accessGroup.getScope().getScopeId()); + String storagePoolUuid = accessGroup.getPrimaryDataStoreInfo().getUuid(); + igroupName = Utility.getIgroupName(svmName, storagePoolUuid); Hypervisor.HypervisorType hypervisorType = accessGroup.getPrimaryDataStoreInfo().getHypervisor(); ProtocolType protocol = ProtocolType.valueOf(dataStoreDetails.get(Constants.PROTOCOL)); @@ -172,24 +268,25 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { } igroupRequest.setInitiators(initiators); } - igroupRequest.setProtocol(Igroup.ProtocolEnum.valueOf("iscsi")); + igroupRequest.setProtocol(Igroup.ProtocolEnum.valueOf(Constants.ISCSI)); // Create Igroup s_logger.debug("createAccessGroup: About to call sanFeignClient.createIgroup with igroupName: {}", igroupName); AccessGroup createdAccessGroup = new AccessGroup(); OntapResponse createdIgroup = null; try { createdIgroup = sanFeignClient.createIgroup(authHeader, true, igroupRequest); - } catch (Exception feignEx) { - String errMsg = feignEx.getMessage(); - if (errMsg != null && errMsg.contains(("5374023"))) { - s_logger.warn("createAccessGroup: Igroup with name {} already exists. Fetching existing Igroup.", igroupName); + } catch (FeignException feignEx) { + if (feignEx.status() == 409) { + s_logger.warn("createAccessGroup: Igroup with name {} already exists (status 409). Fetching existing Igroup.", igroupName); // TODO: Currently we aren't doing anything with the returned AccessGroup object, so, haven't added code here to fetch the existing Igroup and set it in AccessGroup. return createdAccessGroup; } - s_logger.error("createAccessGroup: Exception during Feign call: {}", feignEx.getMessage(), feignEx); + s_logger.error("createAccessGroup: FeignException during Igroup creation: Status: {}, Exception: {}", feignEx.status(), feignEx.getMessage(), feignEx); throw feignEx; } + s_logger.debug("createAccessGroup: createdIgroup: {}", createdIgroup); + s_logger.debug("createAccessGroup: createdIgroup Records: {}", createdIgroup.getRecords()); if (createdIgroup == null || createdIgroup.getRecords() == null || createdIgroup.getRecords().isEmpty()) { s_logger.error("createAccessGroup: Igroup creation failed for Igroup Name {}", igroupName); throw new CloudRuntimeException("Failed to create Igroup: " + igroupName); @@ -226,14 +323,16 @@ public void deleteAccessGroup(AccessGroup accessGroup) { // Extract SVM name from storage (already initialized in constructor via OntapStorage) String svmName = storage.getSvmName(); + String storagePoolUuid = primaryDataStoreInfo.getUuid(); // Determine scope and generate iGroup name - String igroupName; + String igroupName = Utility.getIgroupName(svmName, storagePoolUuid); + s_logger.info("deleteAccessGroup: Generated iGroup name '{}'", igroupName); if (primaryDataStoreInfo.getClusterId() != null) { - igroupName = Utility.getIgroupName(svmName, com.cloud.storage.ScopeType.CLUSTER, primaryDataStoreInfo.getClusterId()); + igroupName = Utility.getIgroupName(svmName, storagePoolUuid); s_logger.info("deleteAccessGroup: Deleting cluster-scoped iGroup '{}'", igroupName); } else { - igroupName = Utility.getIgroupName(svmName, com.cloud.storage.ScopeType.ZONE, primaryDataStoreInfo.getDataCenterId()); + igroupName = Utility.getIgroupName(svmName, storagePoolUuid); s_logger.info("deleteAccessGroup: Deleting zone-scoped iGroup '{}'", igroupName); } @@ -264,16 +363,21 @@ public void deleteAccessGroup(AccessGroup accessGroup) { s_logger.info("deleteAccessGroup: Successfully deleted iGroup '{}'", igroupName); - } catch (Exception e) { - String errorMsg = e.getMessage(); - // Check if iGroup doesn't exist (ONTAP error code: 5374852 - "The initiator group does not exist.") - if (errorMsg != null && (errorMsg.contains("5374852") || errorMsg.contains("not found"))) { - s_logger.warn("deleteAccessGroup: iGroup '{}' does not exist, skipping deletion", igroupName); + } catch (FeignException e) { + if (e.status() == 404) { + s_logger.warn("deleteAccessGroup: iGroup '{}' does not exist (status 404), skipping deletion", igroupName); } else { + s_logger.error("deleteAccessGroup: FeignException occurred: Status: {}, Exception: {}", e.status(), e.getMessage(), e); throw e; } + } catch (Exception e) { + s_logger.error("deleteAccessGroup: Exception occurred: {}", e.getMessage(), e); + throw e; } + } catch (FeignException e) { + s_logger.error("deleteAccessGroup: FeignException occurred while deleting iGroup. Status: {}, Exception: {}", e.status(), e.getMessage(), e); + throw new CloudRuntimeException("Failed to delete iGroup: " + e.getMessage(), e); } catch (Exception e) { s_logger.error("deleteAccessGroup: Failed to delete iGroup. Exception: {}", e.getMessage(), e); throw new CloudRuntimeException("Failed to delete iGroup: " + e.getMessage(), e); @@ -305,19 +409,225 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) { return null; } - @Override - public AccessGroup getAccessGroup(AccessGroup accessGroup) { - //TODO + public AccessGroup getAccessGroup(Map values) { + s_logger.info("getAccessGroup : fetch Igroup"); + s_logger.debug("getAccessGroup : fetching Igroup with params {} ", values); + if (values == null || values.isEmpty()) { + s_logger.error("getAccessGroup: get Igroup failed. Invalid request: {}", values); + throw new CloudRuntimeException("getAccessGroup : get Igroup Failed, invalid request"); + } + String svmName = values.get(Constants.SVM_DOT_NAME); + String igroupName = values.get(Constants.NAME); + if (svmName == null || igroupName == null || svmName.isEmpty() || igroupName.isEmpty()) { + s_logger.error("getAccessGroup: get Igroup failed. Invalid svm:{} or igroup name: {}", svmName, igroupName); + throw new CloudRuntimeException("getAccessGroup : Failed to get Igroup, invalid request"); + } + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + Map queryParams = Map.of(Constants.SVM_DOT_NAME, svmName, Constants.NAME, igroupName, Constants.FIELDS, Constants.INITIATORS); + OntapResponse igroupResponse = sanFeignClient.getIgroupResponse(authHeader, queryParams); + if (igroupResponse == null || igroupResponse.getRecords() == null || igroupResponse.getRecords().isEmpty()) { + s_logger.warn("getAccessGroup: Igroup '{}' not found on SVM '{}'. Returning null.", igroupName, svmName); + return null; + } + Igroup igroup = igroupResponse.getRecords().get(0); + AccessGroup accessGroup = new AccessGroup(); + accessGroup.setIgroup(igroup); + return accessGroup; + } catch (FeignException e) { + if (e.status() == 404) { + s_logger.warn("getAccessGroup: Igroup '{}' not found on SVM '{}' (status 404). Returning null.", igroupName, svmName); + return null; + } + s_logger.error("FeignException occurred while fetching Igroup, Status: {}, Exception: {}", e.status(), e.getMessage()); + throw new CloudRuntimeException("Failed to fetch Igroup details: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception occurred while fetching Igroup, Exception: {}", e.getMessage()); + throw new CloudRuntimeException("Failed to fetch Igroup details: " + e.getMessage()); + } + } + + public Map enableLogicalAccess(Map values) { + s_logger.info("enableLogicalAccess : Create LunMap"); + s_logger.debug("enableLogicalAccess : Creating LunMap with values {} ", values); + Map response = null; + String svmName = values.get(Constants.SVM_DOT_NAME); + String lunName = values.get(Constants.LUN_DOT_NAME); + String igroupName = values.get(Constants.IGROUP_DOT_NAME); + if (svmName == null || lunName == null || igroupName == null || svmName.isEmpty() || lunName.isEmpty() || igroupName.isEmpty()) { + s_logger.error("enableLogicalAccess: LunMap creation failed. Invalid request values: {}", values); + throw new CloudRuntimeException("enableLogicalAccess : Failed to create LunMap, invalid request"); + } + try { + // Get AuthHeader + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + // Create LunMap + LunMap lunMapRequest = new LunMap(); + Svm svm = new Svm(); + svm.setName(svmName); + lunMapRequest.setSvm(svm); + //Set Lun name + Lun lun = new Lun(); + lun.setName(lunName); + lunMapRequest.setLun(lun); + //Set Igroup name + Igroup igroup = new Igroup(); + igroup.setName(igroupName); + lunMapRequest.setIgroup(igroup); + try { + sanFeignClient.createLunMap(authHeader, true, lunMapRequest); + } catch (Exception feignEx) { + String errMsg = feignEx.getMessage(); + if (errMsg != null && errMsg.contains(("LUN already mapped to this group"))) { + s_logger.warn("enableLogicalAccess: LunMap for Lun: {} and igroup: {} already exists.", lunName, igroupName); + } else { + s_logger.error("enableLogicalAccess: Exception during Feign call: {}", feignEx.getMessage(), feignEx); + throw feignEx; + } + } + // Get the LunMap details + OntapResponse lunMapResponse = null; + try { + lunMapResponse = sanFeignClient.getLunMapResponse(authHeader, + Map.of( + Constants.SVM_DOT_NAME, svmName, + Constants.LUN_DOT_NAME, lunName, + Constants.IGROUP_DOT_NAME, igroupName, + Constants.FIELDS, Constants.LOGICAL_UNIT_NUMBER + )); + response = Map.of( + Constants.LOGICAL_UNIT_NUMBER, lunMapResponse.getRecords().get(0).getLogicalUnitNumber().toString() + ); + } catch (Exception e) { + s_logger.error("enableLogicalAccess: Failed to fetch LunMap details for Lun: {} and igroup: {}, Exception: {}", lunName, igroupName, e); + throw new CloudRuntimeException("Failed to fetch LunMap details for Lun: " + lunName + " and igroup: " + igroupName); + } + s_logger.debug("enableLogicalAccess: LunMap created successfully, LunMap: {}", lunMapResponse.getRecords().get(0)); + s_logger.info("enableLogicalAccess: LunMap created successfully."); + } catch (Exception e) { + s_logger.error("Exception occurred while creating LunMap", e); + throw new CloudRuntimeException("Failed to create LunMap: " + e.getMessage()); + } + return response; + } + + public void disableLogicalAccess(Map values) { + s_logger.info("disableLogicalAccess : Delete LunMap"); + s_logger.debug("disableLogicalAccess : Deleting LunMap with values {} ", values); + String lunUUID = values.get(Constants.LUN_DOT_UUID); + String igroupUUID = values.get(Constants.IGROUP_DOT_UUID); + if (lunUUID == null || igroupUUID == null || lunUUID.isEmpty() || igroupUUID.isEmpty()) { + s_logger.error("disableLogicalAccess: LunMap deletion failed. Invalid request values: {}", values); + throw new CloudRuntimeException("disableLogicalAccess : Failed to delete LunMap, invalid request"); + } + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + sanFeignClient.deleteLunMap(authHeader, lunUUID, igroupUUID); + s_logger.info("disableLogicalAccess: LunMap deleted successfully."); + } catch (FeignException e) { + if (e.status() == 404) { + s_logger.warn("disableLogicalAccess: LunMap with Lun UUID: {} and igroup UUID: {} does not exist, skipping deletion", lunUUID, igroupUUID); + return; + } + s_logger.error("FeignException occurred while deleting LunMap, Status: {}, Exception: {}", e.status(), e.getMessage()); + throw new CloudRuntimeException("Failed to delete LunMap: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception occurred while deleting LunMap, Exception: {}", e.getMessage()); + throw new CloudRuntimeException("Failed to delete LunMap: " + e.getMessage()); + } + } + + // GET-only helper: fetch LUN-map and return logical unit number if it exists; otherwise return null + public Map getLogicalAccess(Map values) { + s_logger.info("getLogicalAccess : Fetch LunMap"); + s_logger.debug("getLogicalAccess : Fetching LunMap with values {} ", values); + String svmName = values.get(Constants.SVM_DOT_NAME); + String lunName = values.get(Constants.LUN_DOT_NAME); + String igroupName = values.get(Constants.IGROUP_DOT_NAME); + if (svmName == null || lunName == null || igroupName == null || svmName.isEmpty() || lunName.isEmpty() || igroupName.isEmpty()) { + s_logger.error("getLogicalAccess: Invalid request values: {}", values); + throw new CloudRuntimeException("getLogicalAccess : Invalid request"); + } + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + OntapResponse lunMapResponse = sanFeignClient.getLunMapResponse(authHeader, + Map.of( + Constants.SVM_DOT_NAME, svmName, + Constants.LUN_DOT_NAME, lunName, + Constants.IGROUP_DOT_NAME, igroupName, + Constants.FIELDS, Constants.LOGICAL_UNIT_NUMBER + )); + if (lunMapResponse != null && lunMapResponse.getRecords() != null && !lunMapResponse.getRecords().isEmpty()) { + String lunNumber = lunMapResponse.getRecords().get(0).getLogicalUnitNumber() != null ? + lunMapResponse.getRecords().get(0).getLogicalUnitNumber().toString() : null; + return lunNumber != null ? Map.of(Constants.LOGICAL_UNIT_NUMBER, lunNumber) : null; + } + } catch (Exception e) { + s_logger.warn("getLogicalAccess: LunMap not found for Lun: {} and igroup: {} ({}).", lunName, igroupName, e.getMessage()); + } return null; } @Override - void enableLogicalAccess(Map values) { - //TODO + public String ensureLunMapped(String svmName, String lunName, String accessGroupName) { + s_logger.info("ensureLunMapped: Ensuring LUN [{}] is mapped to igroup [{}] on SVM [{}]", lunName, accessGroupName, svmName); + + // Check existing map first + Map getMap = Map.of( + Constants.LUN_DOT_NAME, lunName, + Constants.SVM_DOT_NAME, svmName, + Constants.IGROUP_DOT_NAME, accessGroupName + ); + Map mapResp = getLogicalAccess(getMap); + if (mapResp != null && mapResp.containsKey(Constants.LOGICAL_UNIT_NUMBER)) { + String lunNumber = mapResp.get(Constants.LOGICAL_UNIT_NUMBER); + s_logger.info("ensureLunMapped: Existing LunMap found for LUN [{}] in igroup [{}] with LUN number [{}]", lunName, accessGroupName, lunNumber); + return lunNumber; + } + + // Create if not exists + Map enableMap = Map.of( + Constants.LUN_DOT_NAME, lunName, + Constants.SVM_DOT_NAME, svmName, + Constants.IGROUP_DOT_NAME, accessGroupName + ); + Map response = enableLogicalAccess(enableMap); + if (response == null || !response.containsKey(Constants.LOGICAL_UNIT_NUMBER)) { + throw new CloudRuntimeException("ensureLunMapped: Failed to map LUN [" + lunName + "] to iGroup [" + accessGroupName + "]"); + } + s_logger.info("ensureLunMapped: Successfully mapped LUN [{}] to igroup [{}] with LUN number [{}]", lunName, accessGroupName, response.get(Constants.LOGICAL_UNIT_NUMBER)); + return response.get(Constants.LOGICAL_UNIT_NUMBER); } @Override - void disableLogicalAccess(Map values) { - //TODO + public boolean validateInitiatorInAccessGroup(String hostInitiator, String svmName, String accessGroupName) { + s_logger.info("validateInitiatorInAccessGroup: Validating initiator [{}] is in igroup [{}] on SVM [{}]", hostInitiator, accessGroupName, svmName); + + if (hostInitiator == null || hostInitiator.isEmpty()) { + s_logger.warn("validateInitiatorInAccessGroup: host initiator is null or empty"); + return false; + } + + Map getAccessGroupMap = Map.of( + Constants.NAME, accessGroupName, + Constants.SVM_DOT_NAME, svmName + ); + AccessGroup accessGroup = getAccessGroup(getAccessGroupMap); + if (accessGroup == null || accessGroup.getIgroup() == null) { + s_logger.warn("validateInitiatorInAccessGroup: iGroup [{}] not found on SVM [{}]", accessGroupName, svmName); + return false; + } + + Igroup igroup = accessGroup.getIgroup(); + if (igroup.getInitiators() != null) { + for (Initiator initiator : igroup.getInitiators()) { + if (initiator.getName().equalsIgnoreCase(hostInitiator)) { + s_logger.info("validateInitiatorInAccessGroup: Initiator [{}] validated successfully in igroup [{}]", hostInitiator, accessGroupName); + return true; + } + } + } + s_logger.warn("validateInitiatorInAccessGroup: Initiator [{}] NOT found in igroup [{}]", hostInitiator, accessGroupName); + return false; } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java index 23425aa6b797..920bd45fd0d7 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java @@ -22,6 +22,10 @@ public class Constants { + public static final String ONTAP_PLUGIN_NAME = "ONTAP"; + public static final int NFS3_PORT = 2049; + public static final int ISCSI_PORT = 3260; + public static final String NFS = "nfs"; public static final String ISCSI = "iscsi"; public static final String SIZE = "size"; @@ -53,6 +57,7 @@ public class Constants { // Query params public static final String NAME = "name"; public static final String FIELDS = "fields"; + public static final String INITIATORS = "initiators"; public static final String AGGREGATES = "aggregates"; public static final String STATE = "state"; public static final String DATA_NFS = "data_nfs"; @@ -80,6 +85,7 @@ public class Constants { public static final String LUN_DOT_NAME = "lun.name"; public static final String IQN = "iqn"; public static final String LUN_DOT_UUID = "lun.uuid"; + public static final String LOGICAL_UNIT_NUMBER = "logical_unit_number"; public static final String IGROUP_DOT_NAME = "igroup.name"; public static final String IGROUP_DOT_UUID = "igroup.uuid"; public static final String UNDERSCORE = "_"; diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java index c20c9d6dd151..2f805c1784d6 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Utility.java @@ -19,7 +19,7 @@ package org.apache.cloudstack.storage.utils; -import com.cloud.storage.ScopeType; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.utils.StringUtils; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; @@ -69,31 +69,27 @@ public static CloudStackVolume createCloudStackVolumeRequestByProtocol(StoragePo cloudStackVolumeRequest.setVolumeInfo(volumeObject); break; case ISCSI: - cloudStackVolumeRequest = new CloudStackVolume(); - Lun lunRequest = new Lun(); Svm svm = new Svm(); svm.setName(details.get(Constants.SVM_NAME)); + cloudStackVolumeRequest = new CloudStackVolume(); + Lun lunRequest = new Lun(); lunRequest.setSvm(svm); LunSpace lunSpace = new LunSpace(); lunSpace.setSize(volumeObject.getSize()); lunRequest.setSpace(lunSpace); //Lun name is full path like in unified "/vol/VolumeName/LunName" - String lunFullName = Constants.VOLUME_PATH_PREFIX + storagePool.getName() + Constants.SLASH + volumeObject.getName(); + String lunName = volumeObject.getName().replace(Constants.HYPHEN, Constants.UNDERSCORE); + if(!isValidName(lunName)) { + String errMsg = "createAsync: Invalid dataObject name [" + lunName + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long."; + throw new InvalidParameterValueException(errMsg); + } + String lunFullName = getLunName(storagePool.getName(), lunName); lunRequest.setName(lunFullName); - String hypervisorType = storagePool.getHypervisor().name(); - String osType = null; - switch (hypervisorType) { - case Constants.KVM: - osType = Lun.OsTypeEnum.LINUX.getValue(); - break; - default: - String errMsg = "createCloudStackVolume : Unsupported hypervisor type " + hypervisorType + " for ONTAP storage"; - s_logger.error(errMsg); - throw new CloudRuntimeException(errMsg); - } + String osType = getOSTypeFromHypervisor(storagePool.getHypervisor().name()); lunRequest.setOsType(Lun.OsTypeEnum.valueOf(osType)); + cloudStackVolumeRequest.setLun(lunRequest); break; default: @@ -103,6 +99,15 @@ public static CloudStackVolume createCloudStackVolumeRequestByProtocol(StoragePo return cloudStackVolumeRequest; } + public static boolean isValidName(String name) { + // Check for null and length constraint first + if (name == null || name.length() > 200) { + return false; + } + // Regex: Starts with a letter, followed by letters, digits, or underscores + return name.matches(Constants.ONTAP_NAME_REGEX); + } + public static String getOSTypeFromHypervisor(String hypervisorType){ switch (hypervisorType) { case Constants.KVM: @@ -135,12 +140,17 @@ public static StorageStrategy getStrategyByStoragePoolDetails(Map dsInfos = new HashMap<>(); + dsInfos.put("username", "testUser"); + dsInfos.put("password", "testPassword"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); + dsInfos.put("zoneId",1L); + dsInfos.put("podId",1L); + dsInfos.put("clusterId", 1L); + dsInfos.put("name", "testStoragePool"); + dsInfos.put("providerName", "testProvider"); + dsInfos.put("capacityBytes",200000L); + dsInfos.put("managed",true); + dsInfos.put("tags", "testTag"); + dsInfos.put("isTagARule", false); + dsInfos.put("details", new HashMap()); + + try(MockedStatic storageProviderFactory = Mockito.mockStatic(StorageProviderFactory.class)) { + storageProviderFactory.when(() -> StorageProviderFactory.getStrategy(any())).thenReturn(storageStrategy); + ontapPrimaryDatastoreLifecycle.initialize(dsInfos); + } + } + + @Test + public void testInitialize_positiveWithIsDisaggregated() { + Map dsInfos = new HashMap<>(); dsInfos.put("username", "testUser"); dsInfos.put("password", "testPassword"); @@ -109,7 +133,7 @@ public void testInitialize_null_Arg() { @Test public void testInitialize_missingRequiredDetailKey() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); @@ -131,7 +155,7 @@ public void testInitialize_missingRequiredDetailKey() { @Test public void testInitialize_invalidCapacityBytes() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); @@ -152,7 +176,7 @@ public void testInitialize_invalidCapacityBytes() { @Test public void testInitialize_unmanagedStorage() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); @@ -176,7 +200,7 @@ public void testInitialize_unmanagedStorage() { @Test public void testInitialize_nullStoragePoolName() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); @@ -200,7 +224,7 @@ public void testInitialize_nullStoragePoolName() { @Test public void testInitialize_nullProviderName() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); @@ -224,7 +248,7 @@ public void testInitialize_nullProviderName() { @Test public void testInitialize_nullPodAndClusterAndZone() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",null); dsInfos.put("podId",null); dsInfos.put("clusterId", null); @@ -252,7 +276,7 @@ public void testInitialize_clusterNotKVM() { when(_clusterDao.findById(2L)).thenReturn(clusterVO); Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 2L); @@ -276,7 +300,7 @@ public void testInitialize_clusterNotKVM() { @Test public void testInitialize_unexpectedDetailKey() { Map dsInfos = new HashMap<>(); - dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;isDisaggregated=false;unexpectedKey=unexpectedValue"); + dsInfos.put("url", "username=testUser;password=testPassword;svmName=testSVM;protocol=NFS3;managementLIF=192.168.1.1;unexpectedKey=unexpectedValue"); dsInfos.put("zoneId",1L); dsInfos.put("podId",1L); dsInfos.put("clusterId", 1L); diff --git a/plugins/storage/volume/ontap/src/test/java/provider/OntapPrimaryDatastoreProviderTest.java b/plugins/storage/volume/ontap/src/test/java/provider/OntapPrimaryDatastoreProviderTest.java new file mode 100644 index 000000000000..dc360f5688dd --- /dev/null +++ b/plugins/storage/volume/ontap/src/test/java/provider/OntapPrimaryDatastoreProviderTest.java @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package provider; + +import com.cloud.utils.component.ComponentContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.DataStoreProviderType; +import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; +import org.apache.cloudstack.storage.driver.OntapPrimaryDatastoreDriver; +import org.apache.cloudstack.storage.lifecycle.OntapPrimaryDatastoreLifecycle; +import org.apache.cloudstack.storage.listener.OntapHostListener; +import org.apache.cloudstack.storage.provider.OntapPrimaryDatastoreProvider; +import org.apache.cloudstack.storage.utils.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class OntapPrimaryDatastoreProviderTest { + + private OntapPrimaryDatastoreProvider provider; + + @BeforeEach + void setUp() { + provider = new OntapPrimaryDatastoreProvider(); + } + + @Test + public void testGetName() { + String name = provider.getName(); + assertEquals(Constants.ONTAP_PLUGIN_NAME, name); + } + + @Test + public void testGetTypes() { + Set types = provider.getTypes(); + assertNotNull(types); + assertEquals(1, types.size()); + assertTrue(types.contains(DataStoreProviderType.PRIMARY)); + } + + @Test + public void testGetDataStoreLifeCycle_beforeConfigure() { + DataStoreLifeCycle lifeCycle = provider.getDataStoreLifeCycle(); + assertNull(lifeCycle); + } + + @Test + public void testGetDataStoreDriver_beforeConfigure() { + DataStoreDriver driver = provider.getDataStoreDriver(); + assertNull(driver); + } + + @Test + public void testGetHostListener_beforeConfigure() { + HypervisorHostListener listener = provider.getHostListener(); + assertNull(listener); + } + + @Test + public void testConfigure() { + OntapPrimaryDatastoreDriver mockDriver = mock(OntapPrimaryDatastoreDriver.class); + OntapPrimaryDatastoreLifecycle mockLifecycle = mock(OntapPrimaryDatastoreLifecycle.class); + OntapHostListener mockListener = mock(OntapHostListener.class); + + try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) { + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreDriver.class)) + .thenReturn(mockDriver); + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreLifecycle.class)) + .thenReturn(mockLifecycle); + componentContext.when(() -> ComponentContext.inject(OntapHostListener.class)) + .thenReturn(mockListener); + + Map params = new HashMap<>(); + boolean result = provider.configure(params); + + assertTrue(result); + } + } + + @Test + public void testGetDataStoreLifeCycle_afterConfigure() { + OntapPrimaryDatastoreDriver mockDriver = mock(OntapPrimaryDatastoreDriver.class); + OntapPrimaryDatastoreLifecycle mockLifecycle = mock(OntapPrimaryDatastoreLifecycle.class); + OntapHostListener mockListener = mock(OntapHostListener.class); + + try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) { + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreDriver.class)) + .thenReturn(mockDriver); + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreLifecycle.class)) + .thenReturn(mockLifecycle); + componentContext.when(() -> ComponentContext.inject(OntapHostListener.class)) + .thenReturn(mockListener); + + provider.configure(new HashMap<>()); + + DataStoreLifeCycle lifeCycle = provider.getDataStoreLifeCycle(); + assertNotNull(lifeCycle); + assertEquals(mockLifecycle, lifeCycle); + } + } + + @Test + public void testGetDataStoreDriver_afterConfigure() { + OntapPrimaryDatastoreDriver mockDriver = mock(OntapPrimaryDatastoreDriver.class); + OntapPrimaryDatastoreLifecycle mockLifecycle = mock(OntapPrimaryDatastoreLifecycle.class); + OntapHostListener mockListener = mock(OntapHostListener.class); + + try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) { + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreDriver.class)) + .thenReturn(mockDriver); + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreLifecycle.class)) + .thenReturn(mockLifecycle); + componentContext.when(() -> ComponentContext.inject(OntapHostListener.class)) + .thenReturn(mockListener); + + provider.configure(new HashMap<>()); + + DataStoreDriver driver = provider.getDataStoreDriver(); + assertNotNull(driver); + assertEquals(mockDriver, driver); + } + } + + @Test + public void testGetHostListener_afterConfigure() { + OntapPrimaryDatastoreDriver mockDriver = mock(OntapPrimaryDatastoreDriver.class); + OntapPrimaryDatastoreLifecycle mockLifecycle = mock(OntapPrimaryDatastoreLifecycle.class); + OntapHostListener mockListener = mock(OntapHostListener.class); + + try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) { + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreDriver.class)) + .thenReturn(mockDriver); + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreLifecycle.class)) + .thenReturn(mockLifecycle); + componentContext.when(() -> ComponentContext.inject(OntapHostListener.class)) + .thenReturn(mockListener); + + provider.configure(new HashMap<>()); + + HypervisorHostListener listener = provider.getHostListener(); + assertNotNull(listener); + assertEquals(mockListener, listener); + } + } + + @Test + public void testConfigure_withNullParams() { + OntapPrimaryDatastoreDriver mockDriver = mock(OntapPrimaryDatastoreDriver.class); + OntapPrimaryDatastoreLifecycle mockLifecycle = mock(OntapPrimaryDatastoreLifecycle.class); + OntapHostListener mockListener = mock(OntapHostListener.class); + + try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) { + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreDriver.class)) + .thenReturn(mockDriver); + componentContext.when(() -> ComponentContext.inject(OntapPrimaryDatastoreLifecycle.class)) + .thenReturn(mockLifecycle); + componentContext.when(() -> ComponentContext.inject(OntapHostListener.class)) + .thenReturn(mockListener); + + boolean result = provider.configure(null); + + assertTrue(result); + assertNotNull(provider.getDataStoreDriver()); + assertNotNull(provider.getDataStoreLifeCycle()); + assertNotNull(provider.getHostListener()); + } + } + + @Test + public void testGetTypes_returnsOnlyPrimaryType() { + Set types = provider.getTypes(); + + assertNotNull(types); + assertEquals(1, types.size()); + assertTrue(types.contains(DataStoreProviderType.PRIMARY)); + + // Verify it doesn't contain other types + for (DataStoreProviderType type : types) { + assertEquals(DataStoreProviderType.PRIMARY, type); + } + } +}