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/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..98c34ef94b72 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.Igroup; +import org.apache.cloudstack.storage.feign.model.Initiator; +import org.apache.cloudstack.storage.feign.model.Lun; import org.apache.cloudstack.storage.service.StorageStrategy; +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,6 +63,7 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -62,7 +73,9 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver { @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"); @@ -86,7 +99,6 @@ public DataTO getTO(DataObject data) { @Override public void createAsync(DataStore dataStore, DataObject dataObject, AsyncCompletionCallback callback) { CreateCmdResult createCmdResult = null; - String path = null; String errMsg = null; if (dataStore == null) { throw new InvalidParameterValueException("createAsync: dataStore should not be null"); @@ -98,12 +110,55 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet 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("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()); + 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 LUN/backing for volume and record relevant details + CloudStackVolume created = createCloudStackVolume(dataStore, volInfo); + + // Immediately ensure LUN-map exists and update VolumeVO path + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + String svmName = details.get(Constants.SVM_NAME); + String lunName = volumeDetailsDao.findDetail(volInfo.getId(), Constants.LUN_DOT_NAME) != null ? + volumeDetailsDao.findDetail(volInfo.getId(), Constants.LUN_DOT_NAME).getValue() : null; + if (lunName == null) { + // Fallback from returned LUN + lunName = created != null && created.getLun() != null ? created.getLun().getName() : null; + } + if (lunName == null) { + throw new CloudRuntimeException("createAsync: Missing LUN name for volume " + volInfo.getId()); + } + long scopeId = (storagePool.getScope() == ScopeType.CLUSTER) ? storagePool.getClusterId() : storagePool.getDataCenterId(); + String lunNumber = ensureLunMapped(storagePool, svmName, lunName, scopeId); + + VolumeVO volumeVO = volumeDao.findById(volInfo.getId()); + if (volumeVO != null) { + String iscsiPath = Constants.SLASH + storagePool.getPath() + Constants.SLASH + lunNumber; + volumeVO.set_iScsiName(iscsiPath); + volumeVO.setPath(iscsiPath); + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + volumeDao.update(volumeVO.getId(), volumeVO); + s_logger.info("createAsync: Volume [{}] iSCSI path set to {}", volumeVO.getId(), iscsiPath); + } + } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + // Ensure pool fields are recorded for managed NFS as well + VolumeVO volumeVO = volumeDao.findById(volInfo.getId()); + if (volumeVO != null) { + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + volumeDao.update(volumeVO.getId(), volumeVO); + s_logger.info("createAsync: Managed NFS volume [{}] associated with pool {}", volumeVO.getId(), storagePool.getId()); + } + } + createCmdResult = new CreateCmdResult(null, new Answer(null, true, null)); } else { errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to createAsync"; s_logger.error(errMsg); @@ -111,39 +166,93 @@ 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) { + private CloudStackVolume createCloudStackVolume(DataStore dataStore, DataObject dataObject) { 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); + CloudStackVolume cloudStackVolume = storageStrategy.createCloudStackVolume(cloudStackVolumeRequest); + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL)) && cloudStackVolume.getLun() != null && cloudStackVolume.getLun().getName() != null) { + s_logger.info("createCloudStackVolume: iSCSI LUN object created for volume [{}]", volumeObject.getId()); + volumeDetailsDao.addDetail(volumeObject.getId(), Constants.LUN_DOT_UUID, cloudStackVolume.getLun().getUuid(), false); + volumeDetailsDao.addDetail(volumeObject.getId(), Constants.LUN_DOT_NAME, cloudStackVolume.getLun().getName(), false); + VolumeVO volumeVO = volumeDao.findById(volumeObject.getId()); + if (volumeVO != null) { + volumeVO.setPath(null); + if (cloudStackVolume.getLun().getUuid() != null) { + volumeVO.setFolder(cloudStackVolume.getLun().getUuid()); + } + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + volumeDao.update(volumeVO.getId(), volumeVO); + } + } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + s_logger.info("createCloudStackVolume: Managed NFS object created for volume [{}]", volumeObject.getId()); + // For Managed NFS, set pool fields on Volume + VolumeVO volumeVO = volumeDao.findById(volumeObject.getId()); + if (volumeVO != null) { + volumeVO.setPoolType(storagePool.getPoolType()); + volumeVO.setPoolId(storagePool.getId()); + volumeDao.update(volumeVO.getId(), volumeVO); + } + } else { + String errMsg = "createCloudStackVolume: Volume creation failed for dataObject: " + volumeObject; + s_logger.error(errMsg); + throw new CloudRuntimeException(errMsg); + } + return cloudStackVolume; } 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()); } } + private String ensureLunMapped(StoragePoolVO storagePool, String svmName, String lunName, long scopeId) { + Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId()); + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); + String accessGroupName = Utility.getIgroupName(svmName, storagePool.getScope(), scopeId); + + // Check existing map first. getLogicalAccess returns null (no exception) when map doesn't exist. + Map getMap = new HashMap<>(); + getMap.put(Constants.LUN_DOT_NAME, lunName); + getMap.put(Constants.SVM_DOT_NAME, svmName); + getMap.put(Constants.IGROUP_DOT_NAME, accessGroupName); + Map mapResp = storageStrategy.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 = new HashMap<>(); + enableMap.put(Constants.LUN_DOT_NAME, lunName); + enableMap.put(Constants.SVM_DOT_NAME, svmName); + enableMap.put(Constants.IGROUP_DOT_NAME, accessGroupName); + Map response = storageStrategy.enableLogicalAccess(enableMap); + if (response == null || !response.containsKey(Constants.LOGICAL_UNIT_NUMBER)) { + throw new CloudRuntimeException("ensureLunMapped: Failed to map LUN [" + lunName + "] to iGroup [" + accessGroupName + "]"); + } + return response.get(Constants.LOGICAL_UNIT_NUMBER); + } + @Override public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) { CommandResult commandResult = new CommandResult(); @@ -161,9 +270,32 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac 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()); + } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); + VolumeInfo volumeObject = (VolumeInfo) data; + s_logger.info("deleteAsync: Deleting volume & LUN for volume id [{}]", volumeObject.getId()); + 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); + // Set the result + commandResult.setResult(null); + commandResult.setSuccess(true); + s_logger.info("deleteAsync: Volume LUN [{}] deleted successfully", lunName); + } else { + throw new CloudRuntimeException("deleteAsync: Unsupported protocol for deletion: " + 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 +304,6 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac @Override public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) { - } @Override @@ -197,11 +328,200 @@ public ChapInfo getChapInfo(DataObject dataObject) { @Override public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore) { + 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"); + } + try { + 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()); + } + 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 : Cloud Stack Volume not found for id: " + dataObject.getId()); + throw new CloudRuntimeException("grantAccess : Cloud Stack 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(); + // Validate initiator membership + validateHostInitiatorInIgroup(storagePool, svmName, scopeId, host); + // Ensure mapping exists + String lunNumber = ensureLunMapped(storagePool, svmName, cloudStackVolumeName, scopeId); + // Update Volume path if missing or changed + String iscsiPath = Constants.SLASH + storagePool.getPath() + Constants.SLASH + lunNumber; + if (volumeVO.getPath() == null || !volumeVO.getPath().equals(iscsiPath)) { + volumeVO.set_iScsiName(iscsiPath); + volumeVO.setPath(iscsiPath); + } + // Ensure pool fields are set (align with SolidFire) + 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"); + } + } catch(Exception e){ + s_logger.error("grantAccess: Failed for dataObject [{}]: {}", dataObject, e.getMessage()); + throw new CloudRuntimeException("grantAccess: Failed with error :" + e.getMessage()); + } return true; } + private void validateHostInitiatorInIgroup(StoragePoolVO storagePool, String svmName, long scopeId, Host host) { + Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId()); + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(details); + String accessGroupName = Utility.getIgroupName(svmName, storagePool.getScope(), scopeId); + AccessGroup accessGroup = getAccessGroupByName(storageStrategy, svmName, accessGroupName); + if (host == null || host.getStorageUrl() == null) { + throw new CloudRuntimeException("validateHostInitiatorInIgroup: host/initiator required but not provided"); + } + if (!hostInitiatorFoundInIgroup(host.getStorageUrl(), accessGroup.getIgroup())) { + s_logger.error("validateHostInitiatorInIgroup: initiator [{}] is not present in iGroup [{}]", host.getStorageUrl(), accessGroupName); + throw new CloudRuntimeException("validateHostInitiatorInIgroup: initiator [" + host.getStorageUrl() + "] is not present in iGroup [" + accessGroupName + "]"); + } + } + + private boolean hostInitiatorFoundInIgroup(String hostInitiator, Igroup igroup) { + if(igroup != null && igroup.getInitiators() != null && hostInitiator != null && !hostInitiator.isEmpty()) { + for(Initiator initiator : igroup.getInitiators()) { + if(initiator.getName().equalsIgnoreCase(hostInitiator)) { + return true; + } + } + } + return false; + } + @Override public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore) { + if (dataStore == null) { + throw new InvalidParameterValueException("revokeAccess: data store should not be null"); + } + if (dataObject == null) { + throw new InvalidParameterValueException("revokeAccess: data object should not be null"); + } + if (host == null) { + throw new InvalidParameterValueException("revokeAccess: host should not be null"); + } + 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.debug("revokeAccess: Volume [{}] is still attached to VM [{}] in state [{}], skipping revokeAccess", + dataObject.getId(), vm.getInstanceName(), vm.getState()); + return; + } + } + } + try { + 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: Cloud Stack Volume not found for id: " + dataObject.getId()); + throw new CloudRuntimeException("revokeAccess : Cloud Stack 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()); + } + } + + 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); + long scopeId = (storagePool.getScope() == ScopeType.CLUSTER) ? host.getClusterId() : host.getDataCenterId(); + + if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + String accessGroupName = Utility.getIgroupName(svmName, storagePool.getScope(), scopeId); + + 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 detail found for volume [{}]; assuming no backend LUN to revoke", volumeVO.getId()); + return; + } + + 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, assuming already deleted", volumeVO.getId()); + return; + } + + 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, assuming already deleted", accessGroupName); + return; + } + + if (!hostInitiatorFoundInIgroup(host.getStorageUrl(), accessGroup.getIgroup())) { + s_logger.error("revokeAccessForVolume: initiator [{}] is not present in iGroup [{}]", host.getStorageUrl(), accessGroupName); + return; + } + + 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); + } + } + + + 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.error("getCloudStackVolumeByName: LUN [{}] not found on ONTAP; returning null", cloudStackVolumeName); + return null; + } + return cloudStackVolume; + } + + 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.error("getAccessGroupByName: iGroup [{}] not found on ONTAP; returning null", 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..6bbd88db14b0 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) @@ -283,30 +285,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 +349,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 +368,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 +403,43 @@ 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; + 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 @@ -400,6 +480,24 @@ public boolean deleteDataStore(DataStore store) { s_logger.info("deleteDataStore: Successfully deleted access groups for storage pool '{}'", storagePool.getName()); + // 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); + } + } catch (Exception e) { s_logger.error("deleteDataStore: Failed to delete access groups for storage pool id: {}. Error: {}", storagePoolId, e.getMessage(), e); @@ -411,7 +509,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..1aadec79b3ab 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 @@ -65,7 +65,7 @@ public HypervisorHostListener getHostListener() { @Override public String getName() { s_logger.trace("OntapPrimaryDatastoreProvider: getName: Called"); - return "ONTAP Primary Datastore Provider"; + return "ONTAP"; } @Override 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..7b20c946d6af 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,55 @@ 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; } - + // TODO: Implement logic to select appropriate aggregate based on storage requirements 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; + } + // Set the aggregates which are according to the storage requirements + for (Aggregate aggr : aggrs) { + s_logger.debug("Found aggregate: " + aggr.getName() + " with UUID: " + aggr.getUuid()); + Aggregate aggrResp = aggregateFeignClient.getAggregateByUUID(authHeader, aggr.getUuid()); + if (!Objects.equals(aggrResp.getState(), Aggregate.StateEnum.ONLINE)) { + s_logger.warn("Aggregate " + aggr.getName() + " is not in online state. Skipping this aggregate."); + continue; + } else if (aggrResp.getSpace() == null || aggrResp.getAvailableBlockStorageSpace() == null || + aggrResp.getAvailableBlockStorageSpace() <= storage.getSize().doubleValue()) { + s_logger.warn("Aggregate " + aggr.getName() + " does not have sufficient available space. Skipping this aggregate."); + continue; + } + s_logger.info("Selected aggregate: " + aggr.getName() + " for volume operations."); + this.aggregates = List.of(aggr); + break; + } + if (this.aggregates == null || this.aggregates.isEmpty()) { + s_logger.error("No suitable aggregates found on SVM " + svmName + " for volume creation."); + 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 +169,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) { @@ -472,26 +494,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 +529,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 +548,38 @@ 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 Nvme/TCP and Nvme/FC protocols + * @param values map including SVM name, LUN name, and igroup name + * @return map containing logical unit number for the new/existing mapping */ - 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 + * //TODO for Nvme/TCP and Nvme/FC protocols + * @param values map including LUN UUID and iGroup UUID + */ + 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) + * //TODO for Nvme/TCP and Nvme/FC protocols + * @param values map with SVM name, LUN name, and igroup name + * @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..fd1f2bb6b84a 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 @@ -27,9 +27,10 @@ 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; @@ -87,7 +88,7 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume createdCloudStackVolume.setLun(lun); return createdCloudStackVolume; } 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 +100,110 @@ 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 (Exception ex) { + String errMsg = ex.getMessage(); + if (errMsg != null && (errMsg.contains("entry doesn't exist") + || errMsg.contains("does not exist") + || errMsg.contains("not found") + || errMsg.contains("status 404"))) { + s_logger.warn("deleteCloudStackVolume: Lun {} does not exist ({}), skipping deletion", cloudstackVolume.getLun().getName(), errMsg); + return; + } + throw ex; + } + 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 (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 (Exception e) { + String errMsg = e.getMessage(); + if (errMsg != null && errMsg.contains("not found")) { + s_logger.warn("getCloudStackVolume: Lun '{}' on SVM '{}' not found ({}). Returning null.", lunName, svmName, errMsg); + return null; + } + s_logger.error("Exception occurred while fetching Lun, Exception: {}", errMsg); + throw new CloudRuntimeException("Failed to fetch Lun details: " + errMsg); + } } @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 @@ -190,6 +279,8 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { 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); @@ -305,19 +396,158 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) { return null; } - @Override - public AccessGroup getAccessGroup(AccessGroup accessGroup) { - //TODO - return null; + 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 (Exception e) { + String errMsg = e.getMessage(); + if (errMsg != null && errMsg.contains("not found")) { + s_logger.warn("getAccessGroup: Igroup '{}' not found on SVM '{}' ({}). Returning null.", igroupName, svmName, errMsg); + return null; + } + s_logger.error("Exception occurred while fetching Igroup, Exception: {}", errMsg); + throw new CloudRuntimeException("Failed to fetch Igroup details: " + errMsg); + } } - @Override - void enableLogicalAccess(Map values) { - //TODO + 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; } - @Override - void disableLogicalAccess(Map values) { - //TODO + 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 (Exception e) { + String errMsg = e.getMessage(); + if (errMsg != null && errMsg.contains("not found")) { + s_logger.warn("disableLogicalAccess: LunMap with Lun UUID: {} and igroup UUID: {} does not exist ({}), skipping deletion", lunUUID, igroupUUID, errMsg); + return; + } + s_logger.error("Exception occurred while deleting LunMap", e); + throw new CloudRuntimeException("Failed to delete LunMap: " + errMsg); + } + } + + // 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; } } 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..43f8511967e7 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 @@ -53,6 +53,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 +81,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..9d5eac8b2cea 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,6 +19,7 @@ package org.apache.cloudstack.storage.utils; +import com.cloud.exception.InvalidParameterValueException; import com.cloud.storage.ScopeType; import com.cloud.utils.StringUtils; import com.cloud.utils.exception.CloudRuntimeException; @@ -69,31 +70,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 +100,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: @@ -136,11 +142,16 @@ public static StorageStrategy getStrategyByStoragePoolDetails(Map