diff --git a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java index 7132d61ed3b0..8436d841376f 100644 --- a/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java +++ b/engine/storage/volume/src/main/java/org/apache/cloudstack/storage/volume/VolumeServiceImpl.java @@ -730,7 +730,16 @@ protected Void managedCopyBaseImageCallback(AsyncCallbackDispatcher/ 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); - createCmdResult = new CreateCmdResult(null, new Answer(null, true, null)); + s_logger.info("createAsync: Created LUN [{}] for volume [{}]. LUN mapping will occur during grantAccess() to per-host igroup.", + lunName, volumeVO.getId()); + // Path will be set during grantAccess when LUN is mapped and we get the LUN ID + // Return LUN name as identifier for CloudStack tracking + volumeVO.set_iScsiName(null); + volumeVO.setPath(null); + createCmdResult = new CreateCmdResult(lunName, new Answer(null, true, null)); } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + // For NFS, set path to volume UUID to ensure uniqueness + // This prevents multiple VMs from using the same template file path + volumeVO.setPath(volInfo.getUuid()); createCmdResult = new CreateCmdResult(volInfo.getUuid(), new Answer(null, true, null)); - s_logger.info("createAsync: Managed NFS volume [{}] associated with pool {}", - volumeVO.getId(), storagePool.getId()); + s_logger.info("createAsync: Managed NFS volume [{}] with path [{}] associated with pool {}", + volumeVO.getId(), volInfo.getUuid(), storagePool.getId()); } volumeDao.update(volumeVO.getId(), volumeVO); } @@ -319,12 +321,33 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore // Only retrieve LUN name for iSCSI volumes String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), Constants.LUN_DOT_NAME).getValue(); 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 + "]"); + String accessGroupName = Utility.getIgroupName(svmName, host.getName()); + + // Validate if Igroup exist ONTAP for this host as we may be using delete_on_unmap= true and igroup may be deleted by ONTAP automatically + Map getAccessGroupMap = Map.of( + Constants.NAME, accessGroupName, + Constants.SVM_DOT_NAME, svmName + ); + Igroup igroup = new Igroup(); + AccessGroup accessGroup = sanStrategy.getAccessGroup(getAccessGroupMap); + if(accessGroup == null || accessGroup.getIgroup() == null) { + s_logger.info("grantAccess: Igroup does not exist for the host: Need to create Igroup " + host.getName()); + // create the igroup for the host and perform lun-mapping + accessGroup = new AccessGroup(); + List hosts = new ArrayList<>(); + hosts.add((HostVO) host); + accessGroup.setHostsToConnect(hosts); + accessGroup.setStoragePoolId(storagePool.getId()); + accessGroup = sanStrategy.createAccessGroup(accessGroup); + }else{ + s_logger.info("grantAccess: Igroup {} already exist for the host {}: ", accessGroup.getIgroup().getName() ,host.getName()); + igroup = accessGroup.getIgroup(); + // TODO + // Verify host initiator is registered in the igroup before allowing access +// if (sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroup.getIgroup())) { +// // add host initiator to the igroup ? or fail here ? +// } + // Use the existing igroup and perform lun-mapping } // Create or retrieve existing LUN mapping @@ -453,7 +476,7 @@ private void revokeAccessForVolume(StoragePoolVO storagePool, VolumeVO volumeVO, // 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())) { + if (!sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroup.getIgroup())) { s_logger.warn("revokeAccessForVolume: Initiator [{}] is not in iGroup [{}], skipping revoke", host.getStorageUrl(), accessGroupName); return; @@ -524,7 +547,6 @@ public long getUsedIops(StoragePool storagePool) { @Override public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) { - } @Override @@ -569,7 +591,6 @@ public boolean isVmInfoNeeded() { @Override public void provideVmInfo(long vmId, long volumeId) { - } @Override 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 75f8301b8b2b..8c60aef0ba76 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 @@ -120,7 +120,7 @@ public DataStore initialize(Map dsInfos) { throw new CloudRuntimeException("Cluster Id or Pod Id is null, cannot create primary storage"); } - if (podId == null && clusterId == null) { + if (podId == null) { if (zoneId != null) { s_logger.info("Both Pod Id and Cluster Id are null, Primary storage pool will be associated with a Zone"); } else { @@ -231,7 +231,7 @@ public DataStore initialize(Map dsInfos) { path = Constants.SLASH + storagePoolName; port = Constants.NFS3_PORT; // Force NFSv3 for ONTAP managed storage to avoid NFSv4 ID mapping issues - details.put("nfsmountopts", "vers=3"); + details.put(Constants.NFS_MOUNT_OPTIONS,Constants.NFS3_MOUNT_OPTIONS_VER_3); s_logger.info("Setting NFS path for storage pool: " + path + ", port: " + port + " with mount option: vers=3"); break; case ISCSI: @@ -281,8 +281,11 @@ public boolean attachCluster(DataStore dataStore, ClusterScope scope) { } PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore; List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInClusterForStorageConnection(primaryStore); - // 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()); + if(hostsToConnect.isEmpty()) { + s_logger.info("attachCluster: No hosts found for primary storage"); + throw new CloudRuntimeException("attachCluster: No hosts found for primary storage"); + } Map details = storagePoolDetailsDao.listDetailsKeyPairs(primaryStore.getId()); StorageStrategy strategy = Utility.getStrategyByStoragePoolDetails(details); @@ -294,22 +297,24 @@ public boolean attachCluster(DataStore dataStore, ClusterScope scope) { 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()); + // We need to create export policy at pool level and igroup at host level(in grantAccess) + if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + if (!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.setStoragePoolId(storagePool.getId()); + 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 { @@ -347,12 +352,14 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore; List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInZoneForStorageConnection(dataStore, scope.getScopeId(), Hypervisor.HypervisorType.KVM); logger.debug(String.format("In createPool. Attaching the pool to each of the hosts in %s.", hostsToConnect)); + if(hostsToConnect.isEmpty()) { + s_logger.info("attachCluster: No hosts found for primary storage"); + throw new CloudRuntimeException("attachCluster: No hosts found for primary storage"); + } Map details = storagePoolDetailsDao.listDetailsKeyPairs(primaryStore.getId()); StorageStrategy strategy = Utility.getStrategyByStoragePoolDetails(details); - // 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)) { @@ -360,17 +367,21 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper 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()); + + // We need to create export policy at pool level and igroup at host level + if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(Constants.PROTOCOL))) { + if (!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.setStoragePoolId(storagePool.getId()); + 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) { @@ -485,7 +496,7 @@ public boolean deleteDataStore(DataStore store) { storagePoolId, e.getMessage(), e); } AccessGroup accessGroup = new AccessGroup(); - accessGroup.setPrimaryDataStoreInfo(primaryDataStoreInfo); + accessGroup.setStoragePoolId(storagePoolId); // Delete access groups associated with this storage pool storageStrategy.deleteAccessGroup(accessGroup); s_logger.info("deleteDataStore: Successfully deleted access groups for storage pool '{}'", storagePool.getName()); 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 6be5ecfaf3f2..1917c1f16703 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 @@ -19,9 +19,14 @@ package org.apache.cloudstack.storage.service; +import org.apache.cloudstack.storage.feign.model.Igroup; +import org.apache.cloudstack.storage.feign.model.Initiator; import org.apache.cloudstack.storage.feign.model.OntapStorage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; public abstract class SANStrategy extends StorageStrategy { + private static final Logger s_logger = LogManager.getLogger(SANStrategy.class); public SANStrategy(OntapStorage ontapStorage) { super(ontapStorage); } @@ -46,5 +51,22 @@ public SANStrategy(OntapStorage ontapStorage) { * @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); + public boolean validateInitiatorInAccessGroup(String hostInitiator, String svmName, Igroup igroup) { + s_logger.info("validateInitiatorInAccessGroup: Validating initiator [{}] is in igroup [{}] on SVM [{}]", hostInitiator, igroup, svmName); + + if (hostInitiator == null || hostInitiator.isEmpty()) { + s_logger.warn("validateInitiatorInAccessGroup: host initiator is null or empty"); + return false; + } + if (igroup.getInitiators() != null) { + for (Initiator initiator : igroup.getInitiators()) { + if (initiator.getName().equalsIgnoreCase(hostInitiator)) { + s_logger.info("validateInitiatorInAccessGroup: Initiator [{}] validated successfully in igroup [{}]", hostInitiator, igroup); + return true; + } + } + } + s_logger.warn("validateInitiatorInAccessGroup: Initiator [{}] NOT found in igroup [{}]", hostInitiator, igroup); + return false; + } } 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 d9f98dcf7cb1..0e0066f42ff4 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 @@ -265,23 +265,10 @@ public Volume createStorageVolume(String volumeName, Long size) { OntapResponse ontapVolume = volumeFeignClient.getVolume(authHeader, queryParams); s_logger.debug("Feign call completed. Processing response..."); - if (ontapVolume == null) { + if (ontapVolume == null || ontapVolume.getRecords() == null || ontapVolume.getRecords().isEmpty()) { s_logger.error("OntapResponse is null for volume: " + volumeName); throw new CloudRuntimeException("Failed to fetch volume " + volumeName + ": Response is null"); } - s_logger.debug("OntapResponse is not null. Checking records field..."); - - if (ontapVolume.getRecords() == null) { - s_logger.error("OntapResponse.records is null for volume: " + volumeName); - throw new CloudRuntimeException("Failed to fetch volume " + volumeName + ": Records list is null"); - } - s_logger.debug("Records field is not null. Size: " + ontapVolume.getRecords().size()); - - if (ontapVolume.getRecords().isEmpty()) { - s_logger.error("OntapResponse.records is empty for volume: " + volumeName); - throw new CloudRuntimeException("Failed to fetch volume " + volumeName + ": No records found"); - } - Volume volume = ontapVolume.getRecords().get(0); s_logger.info("Volume retrieved successfully: " + volumeName + ", UUID: " + volume.getUuid()); return volume; @@ -291,26 +278,11 @@ public Volume createStorageVolume(String volumeName, Long size) { } } - /** - * Updates ONTAP Flex-Volume - * Eligible only for Unified ONTAP storage - * throw exception in case of disaggregated ONTAP storage - * - * @param volume the volume to update - * @return the updated Volume object - */ public Volume updateStorageVolume(Volume volume) { //TODO return null; } - /** - * Delete ONTAP Flex-Volume - * Eligible only for Unified ONTAP storage - * throw exception in case of disaggregated ONTAP storage - * - * @param volume the volume to delete - */ public void deleteStorageVolume(Volume volume) { s_logger.info("Deleting ONTAP volume by name: " + volume.getName() + " and uuid: " + volume.getUuid()); // Calling the VolumeFeignClient to delete the volume @@ -331,14 +303,6 @@ public void deleteStorageVolume(Volume volume) { s_logger.info("ONTAP volume deletion process completed for volume: " + volume.getName()); } - /** - * Gets ONTAP Flex-Volume - * Eligible only for Unified ONTAP storage - * throw exception in case of disaggregated ONTAP storage - * - * @param volume the volume to retrieve - * @return the retrieved Volume object - */ public Volume getStorageVolume(Volume volume) { //TODO return null; 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 c2aa4e462d2f..eb9a69ffd5e8 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 @@ -29,9 +29,10 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.storage.command.CreateObjectCommand; import org.apache.cloudstack.storage.command.DeleteCommand; +import com.cloud.agent.api.to.DataTO; +import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.feign.FeignClientFactory; import org.apache.cloudstack.storage.feign.client.JobFeignClient; @@ -88,10 +89,10 @@ public void setOntapStorage(OntapStorage ontapStorage) { public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume) { s_logger.info("createCloudStackVolume: Create cloudstack volume " + cloudstackVolume); try { - // Step 1: set cloudstack volume metadata + // Step 1: set cloudstack volume metadata and get the volumeUuid String volumeUuid = updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), cloudstackVolume.getVolumeInfo()); - // Step 2: Send command to KVM host to create qcow2 file using qemu-img - Answer answer = createVolumeOnKVMHost(cloudstackVolume.getVolumeInfo()); + // Step 2: Send command to KVM host with explicit volumeUuid to avoid stale cached path + Answer answer = createVolumeOnKVMHost(cloudstackVolume.getVolumeInfo(), volumeUuid); if (answer == null || !answer.getResult()) { String errMsg = answer != null ? answer.getDetails() : "Failed to create qcow2 on KVM host"; s_logger.error("createCloudStackVolume: " + errMsg); @@ -140,7 +141,8 @@ public CloudStackVolume getCloudStackVolume(Map cloudStackVolume @Override public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.info("createAccessGroup: Create access group {}: " , accessGroup); - Map details = accessGroup.getPrimaryDataStoreInfo().getDetails(); + + Map details = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId()); String svmName = details.get(Constants.SVM_NAME); String volumeUUID = details.get(Constants.VOLUME_UUID); String volumeName = details.get(Constants.VOLUME_NAME); @@ -153,8 +155,8 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { // attach export policy to volume of storage pool assignExportPolicyToVolume(volumeUUID,createdPolicy.getName()); // save the export policy details in storage pool details - storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_ID, String.valueOf(createdPolicy.getId()), true); - storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_NAME, createdPolicy.getName(), true); + storagePoolDetailsDao.addDetail(accessGroup.getStoragePoolId(), Constants.EXPORT_POLICY_ID, String.valueOf(createdPolicy.getId()), true); + storagePoolDetailsDao.addDetail(accessGroup.getStoragePoolId(), Constants.EXPORT_POLICY_NAME, createdPolicy.getName(), true); s_logger.info("Successfully assigned exportPolicy {} to volume {}", policyRequest.getName(), volumeName); accessGroup.setPolicy(policyRequest); return accessGroup; @@ -172,18 +174,12 @@ public void deleteAccessGroup(AccessGroup accessGroup) { throw new CloudRuntimeException("deleteAccessGroup: Invalid accessGroup object - accessGroup is null"); } - // Get PrimaryDataStoreInfo from accessGroup - PrimaryDataStoreInfo primaryDataStoreInfo = accessGroup.getPrimaryDataStoreInfo(); - if (primaryDataStoreInfo == null) { - throw new CloudRuntimeException("deleteAccessGroup: PrimaryDataStoreInfo is null in accessGroup"); - } - s_logger.info("deleteAccessGroup: Deleting export policy for the storage pool {}", primaryDataStoreInfo.getName()); try { + Map details = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId()); String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); - String svmName = storage.getSvmName(); // Determine export policy attached to the storage pool - String exportPolicyName = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_NAME); - String exportPolicyId = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_ID); + String exportPolicyName = details.get(Constants.EXPORT_POLICY_NAME); + String exportPolicyId = details.get(Constants.EXPORT_POLICY_ID); try { nasFeignClient.deleteExportPolicyById(authHeader,exportPolicyId); @@ -486,8 +482,9 @@ private String updateCloudStackVolumeMetadata(String dataStoreId, DataObject vol String volumeUuid = volumeInfo.getUuid(); volume.setPoolType(Storage.StoragePoolType.NetworkFilesystem); volume.setPoolId(Long.parseLong(dataStoreId)); - volume.setPath(volumeUuid); // Filename for qcow2 file + volume.setPath(volumeUuid); // Filename for qcow2 file - unique per volume volumeDao.update(volume.getId(), volume); + s_logger.info("Updated volume path to {} for volume ID {}", volumeUuid, volumeId); return volumeUuid; }catch (Exception e){ s_logger.error("Exception while updating volumeInfo: {} in volume: {}", dataStoreId, volumeInfo.getUuid(), e); @@ -495,12 +492,19 @@ private String updateCloudStackVolumeMetadata(String dataStoreId, DataObject vol } } - private Answer createVolumeOnKVMHost(DataObject volumeInfo) { - s_logger.info("createVolumeOnKVMHost called with volumeInfo: {} ", volumeInfo); + private Answer createVolumeOnKVMHost(DataObject volumeInfo, String correctPath) { + s_logger.info("createVolumeOnKVMHost called with volumeInfo: {}, correctPath: {}", volumeInfo, correctPath); try { + // Get the base TO and override the path with correct value to avoid stale cached path + DataTO dataTO = volumeInfo.getTO(); + if (dataTO instanceof VolumeObjectTO) { + VolumeObjectTO volumeTO = (VolumeObjectTO) dataTO; + s_logger.info("createVolumeOnKVMHost: Original TO path: {}, overriding with correct path: {}", volumeTO.getPath(), correctPath); + volumeTO.setPath(correctPath); // Override stale path with correct UUID + } s_logger.info("createVolumeOnKVMHost: Sending CreateObjectCommand to KVM agent for volume: {}", volumeInfo.getUuid()); - CreateObjectCommand cmd = new CreateObjectCommand(volumeInfo.getTO()); + CreateObjectCommand cmd = new CreateObjectCommand(dataTO); EndPoint ep = epSelector.select(volumeInfo); if (ep == null) { String errMsg = "No remote endpoint to send CreateObjectCommand, check if host is up"; 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 c42e5cb6f516..d79d6c44e669 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 @@ -20,10 +20,9 @@ package org.apache.cloudstack.storage.service; 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.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.feign.FeignClientFactory; import org.apache.cloudstack.storage.feign.client.SANFeignClient; import org.apache.cloudstack.storage.feign.model.Igroup; @@ -41,6 +40,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import javax.inject.Inject; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,6 +51,8 @@ public class UnifiedSANStrategy extends SANStrategy { // Replace @Inject Feign client with FeignClientFactory private final FeignClientFactory feignClientFactory; private final SANFeignClient sanFeignClient; + @Inject + private StoragePoolDetailsDao storagePoolDetailsDao; public UnifiedSANStrategy(OntapStorage ontapStorage) { super(ontapStorage); @@ -204,40 +206,33 @@ public CloudStackVolume getCloudStackVolume(Map values) { @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) { s_logger.error("createAccessGroup: Igroup creation failed. Invalid request: {}", accessGroup); throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, invalid request"); } + // Get StoragePool details + if (accessGroup.getStoragePoolId() == null) { + throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, invalid datastore details in the request"); + } + if (accessGroup.getHostsToConnect() == null || accessGroup.getHostsToConnect().isEmpty()) { + throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, no hosts to connect provided in the request"); + } + + String igroupName = null; try { - // Get StoragePool details - if (accessGroup.getPrimaryDataStoreInfo() == null || accessGroup.getPrimaryDataStoreInfo().getDetails() == null - || accessGroup.getPrimaryDataStoreInfo().getDetails().isEmpty()) { - throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, invalid datastore details in the request"); - } - Map dataStoreDetails = accessGroup.getPrimaryDataStoreInfo().getDetails(); + Map dataStoreDetails = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId()); s_logger.debug("createAccessGroup: Successfully fetched datastore details."); - // Get AuthHeader - String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); - // Generate Igroup request Igroup igroupRequest = new Igroup(); - List hostsIdentifier = new ArrayList<>(); String svmName = dataStoreDetails.get(Constants.SVM_NAME); - String storagePoolUuid = accessGroup.getPrimaryDataStoreInfo().getUuid(); - igroupName = Utility.getIgroupName(svmName, storagePoolUuid); - Hypervisor.HypervisorType hypervisorType = accessGroup.getPrimaryDataStoreInfo().getHypervisor(); - ProtocolType protocol = ProtocolType.valueOf(dataStoreDetails.get(Constants.PROTOCOL)); + // Check if all hosts support the protocol - if (accessGroup.getHostsToConnect() == null || accessGroup.getHostsToConnect().isEmpty()) { - throw new CloudRuntimeException("createAccessGroup : Failed to create Igroup, no hosts to connect provided in the request"); - } - if (!validateProtocolSupportAndFetchHostsIdentifier(accessGroup.getHostsToConnect(), protocol, hostsIdentifier)) { - String errMsg = "createAccessGroup: Not all hosts in the " + accessGroup.getScope().getScopeType().toString() + " support the protocol: " + protocol.name(); + List hostIdentifiers = new ArrayList<>(); + if (!validateProtocolSupport(accessGroup.getHostsToConnect(), protocol)) { + String errMsg = "createAccessGroup: Not all hosts " + " support the protocol: " + protocol.name(); throw new CloudRuntimeException(errMsg); } @@ -246,26 +241,17 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { svm.setName(svmName); igroupRequest.setSvm(svm); } + // TODO: Defaulting to LINUX for zone scope for now, this has to be revisited when we support other hypervisors + igroupRequest.setOsType(Igroup.OsTypeEnum.linux); - if (igroupName != null && !igroupName.isEmpty()) { + for (HostVO host : accessGroup.getHostsToConnect()) { + igroupName = Utility.getIgroupName(svmName, host.getName()); igroupRequest.setName(igroupName); - } - -// if (hypervisorType != null) { -// String hypervisorName = hypervisorType.name(); -// igroupRequest.setOsType(Igroup.OsTypeEnum.valueOf(Utility.getOSTypeFromHypervisor(hypervisorName))); -// } else if ( accessGroup.getScope().getScopeType() == ScopeType.ZONE) { -// igroupRequest.setOsType(Igroup.OsTypeEnum.linux); // TODO: Defaulting to LINUX for zone scope for now, this has to be revisited when we support other hypervisors -// } - igroupRequest.setOsType(Igroup.OsTypeEnum.linux); - if (hostsIdentifier != null && hostsIdentifier.size() > 0) { List initiators = new ArrayList<>(); - for (String hostIdentifier : hostsIdentifier) { - Initiator initiator = new Initiator(); - initiator.setName(hostIdentifier); - initiators.add(initiator); - } + Initiator initiator = new Initiator(); + initiator.setName(host.getStorageUrl());// CloudStack has one iqn for one host + initiators.add(initiator); igroupRequest.setInitiators(initiators); } igroupRequest.setProtocol(Igroup.ProtocolEnum.valueOf(Constants.ISCSI)); @@ -274,6 +260,8 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { AccessGroup createdAccessGroup = new AccessGroup(); OntapResponse createdIgroup = null; try { + // Get AuthHeader + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); createdIgroup = sanFeignClient.createIgroup(authHeader, true, igroupRequest); } catch (FeignException feignEx) { if (feignEx.status() == 409) { @@ -287,7 +275,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.debug("createAccessGroup: createdIgroup: {}", createdIgroup); s_logger.debug("createAccessGroup: createdIgroup Records: {}", createdIgroup.getRecords()); - if (createdIgroup == null || createdIgroup.getRecords() == null || createdIgroup.getRecords().isEmpty()) { + if (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); } @@ -309,72 +297,64 @@ public void deleteAccessGroup(AccessGroup accessGroup) { s_logger.info("deleteAccessGroup: Deleting iGroup"); if (accessGroup == null) { - throw new CloudRuntimeException("deleteAccessGroup: Invalid accessGroup object - accessGroup is null"); + s_logger.error("deleteAccessGroup: Igroup deletion failed. Invalid request: {}", accessGroup); + throw new CloudRuntimeException("deleteAccessGroup : Failed to delete Igroup, invalid request"); } - - // Get PrimaryDataStoreInfo from accessGroup - PrimaryDataStoreInfo primaryDataStoreInfo = accessGroup.getPrimaryDataStoreInfo(); - if (primaryDataStoreInfo == null) { - throw new CloudRuntimeException("deleteAccessGroup: PrimaryDataStoreInfo is null in accessGroup"); + // Get StoragePool details + if (accessGroup.getStoragePoolId() == null) { + throw new CloudRuntimeException("deleteAccessGroup : Failed to delete Igroup, invalid datastore details in the request"); + } + if (accessGroup.getHostsToConnect() == null || accessGroup.getHostsToConnect().isEmpty()) { + throw new CloudRuntimeException("deleteAccessGroup : Failed to delete Igroup, no hosts to connect provided in the request"); } try { String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); - - // 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 = Utility.getIgroupName(svmName, storagePoolUuid); - s_logger.info("deleteAccessGroup: Generated iGroup name '{}'", igroupName); - if (primaryDataStoreInfo.getClusterId() != null) { - igroupName = Utility.getIgroupName(svmName, storagePoolUuid); - s_logger.info("deleteAccessGroup: Deleting cluster-scoped iGroup '{}'", igroupName); - } else { - igroupName = Utility.getIgroupName(svmName, storagePoolUuid); - s_logger.info("deleteAccessGroup: Deleting zone-scoped iGroup '{}'", igroupName); - } - - // Get the iGroup to retrieve its UUID - Map igroupParams = Map.of( - Constants.SVM_DOT_NAME, svmName, - Constants.NAME, igroupName - ); + //Get iGroup name per host + for(HostVO host : accessGroup.getHostsToConnect()) { + String igroupName = Utility.getIgroupName(svmName, host.getName()); + s_logger.info("deleteAccessGroup: iGroup name '{}'", igroupName); + + // Get the iGroup to retrieve its UUID + Map igroupParams = Map.of( + Constants.SVM_DOT_NAME, svmName, + Constants.NAME, igroupName + ); - try { - OntapResponse igroupResponse = sanFeignClient.getIgroupResponse(authHeader, igroupParams); - if (igroupResponse == null || igroupResponse.getRecords() == null || igroupResponse.getRecords().isEmpty()) { - s_logger.warn("deleteAccessGroup: iGroup '{}' not found, may have been already deleted", igroupName); - return; - } + try { + OntapResponse igroupResponse = sanFeignClient.getIgroupResponse(authHeader, igroupParams); + if (igroupResponse == null || igroupResponse.getRecords() == null || igroupResponse.getRecords().isEmpty()) { + s_logger.warn("deleteAccessGroup: iGroup '{}' not found, may have been already deleted", igroupName); + return; + } - Igroup igroup = igroupResponse.getRecords().get(0); - String igroupUuid = igroup.getUuid(); + Igroup igroup = igroupResponse.getRecords().get(0); + String igroupUuid = igroup.getUuid(); - if (igroupUuid == null || igroupUuid.isEmpty()) { - throw new CloudRuntimeException("deleteAccessGroup: iGroup UUID is null or empty for iGroup: " + igroupName); - } + if (igroupUuid == null || igroupUuid.isEmpty()) { + throw new CloudRuntimeException("deleteAccessGroup: iGroup UUID is null or empty for iGroup: " + igroupName); + } - s_logger.info("deleteAccessGroup: Deleting iGroup '{}' with UUID '{}'", igroupName, igroupUuid); + s_logger.info("deleteAccessGroup: Deleting iGroup '{}' with UUID '{}'", igroupName, igroupUuid); - // Delete the iGroup using the UUID - sanFeignClient.deleteIgroup(authHeader, igroupUuid); + // Delete the iGroup using the UUID + sanFeignClient.deleteIgroup(authHeader, igroupUuid); - s_logger.info("deleteAccessGroup: Successfully deleted iGroup '{}'", igroupName); + s_logger.info("deleteAccessGroup: Successfully deleted iGroup '{}'", 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); + } 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 (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); @@ -384,20 +364,13 @@ public void deleteAccessGroup(AccessGroup accessGroup) { } } - 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()); + private boolean validateProtocolSupport(List hosts, ProtocolType protocolType) { + 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()); } s_logger.info("validateProtocolSupportAndFetchHostsIdentifier: All hosts support the protocol: " + protocolType.name()); return true; @@ -611,36 +584,4 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup 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 - 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/service/model/AccessGroup.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/AccessGroup.java index 9ff80e7cf8a9..975a74df85aa 100755 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/AccessGroup.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/AccessGroup.java @@ -20,7 +20,6 @@ package org.apache.cloudstack.storage.service.model; import com.cloud.host.HostVO; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import org.apache.cloudstack.storage.feign.model.ExportPolicy; import org.apache.cloudstack.storage.feign.model.Igroup; @@ -33,7 +32,7 @@ public class AccessGroup { private ExportPolicy exportPolicy; private List hostsToConnect; - private PrimaryDataStoreInfo primaryDataStoreInfo; + private Long storagePoolId; private Scope scope; @@ -58,12 +57,15 @@ public List getHostsToConnect() { public void setHostsToConnect(List hostsToConnect) { this.hostsToConnect = hostsToConnect; } - public PrimaryDataStoreInfo getPrimaryDataStoreInfo() { - return primaryDataStoreInfo; + + public Long getStoragePoolId() { + return storagePoolId; } - public void setPrimaryDataStoreInfo(PrimaryDataStoreInfo primaryDataStoreInfo) { - this.primaryDataStoreInfo = primaryDataStoreInfo; + + public void setStoragePoolId(Long storagePoolId) { + this.storagePoolId = storagePoolId; } + public Scope getScope() { return scope; } 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 5e8729ad1917..22d38a14bfc3 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 @@ -42,6 +42,8 @@ public class Constants { public static final String IS_DISAGGREGATED = "isDisaggregated"; public static final String RUNNING = "running"; public static final String EXPORT = "export"; + public static final String NFS_MOUNT_OPTIONS = "nfsmountopts"; + public static final String NFS3_MOUNT_OPTIONS_VER_3 = "vers=3"; public static final int ONTAP_PORT = 443; 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 c8f3b924ae22..930bd34695f5 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 @@ -139,9 +139,10 @@ public static StorageStrategy getStrategyByStoragePoolDetails(Map