diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 689275cff115..375912667d63 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,7 +17,7 @@ /plugins/storage/volume/linstor @rp- /plugins/storage/volume/storpool @slavkap -/plugins/storage/volume/ontap @rajiv1 @sandeeplocharla @piyush5 @suryag +/plugins/storage/volume/ontap @rajiv-jain-netapp @sandeeplocharla @piyush5netapp @suryag1201 .pre-commit-config.yaml @jbampton /.github/linters/ @jbampton diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java index 41c1d9407454..7ff206db3c99 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/subsystem/api/storage/DataStoreProvider.java @@ -29,6 +29,11 @@ public interface DataStoreProvider { String SAMPLE_IMAGE = "Sample"; String SMB = "NFS"; String DEFAULT_PRIMARY = "DefaultPrimary"; + /** + * Primary storage provider name for NetApp ONTAP ({@code OntapPrimaryDatastoreProvider#getName}). + * Single canonical value; use this instead of duplicating the string across modules. + */ + String ONTAP_PLUGIN_NAME = "NetApp ONTAP"; enum DataStoreProviderType { PRIMARY, IMAGE, ImageCache, OBJECT diff --git a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java index d893304cc197..655e0f961db5 100644 --- a/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java +++ b/engine/storage/snapshot/src/main/java/org/apache/cloudstack/storage/vmsnapshot/KvmFileBasedStorageVmSnapshotStrategy.java @@ -51,6 +51,7 @@ import com.cloud.vm.snapshot.VMSnapshotVO; import org.apache.cloudstack.backup.BackupOfferingVO; import org.apache.cloudstack.backup.dao.BackupOfferingDao; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine; import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; @@ -77,8 +78,6 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra private static final List supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.SharedMountPoint); - private static final String ONTAP_PROVIDER_NAME = "NetApp ONTAP"; - @Inject protected SnapshotDataStoreDao snapshotDataStoreDao; @@ -327,13 +326,13 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe List volumes = volumeDao.findByInstance(vmId); for (VolumeVO volume : volumes) { StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId()); - if (storagePoolVO.isManaged() && ONTAP_PROVIDER_NAME.equals(storagePoolVO.getStorageProviderName())) { - logger.debug(String.format("%s as the VM has a volume on ONTAP managed storage pool [%s]. " + - "ONTAP managed storage has its own dedicated VM snapshot strategy.", cantHandleLog, storagePoolVO.getName())); + if (storagePoolVO.isManaged() && DataStoreProvider.ONTAP_PLUGIN_NAME.equals(storagePoolVO.getStorageProviderName())) { + logger.debug("{} as the VM has a volume on ONTAP managed storage pool [{}]. " + + "ONTAP managed storage has its own dedicated VM snapshot strategy.", cantHandleLog, storagePoolVO.getName()); return StrategyPriority.CANT_HANDLE; } if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) { - logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType())); + logger.debug("{} as the VM has a volume that is in a storage with unsupported type [{}].", cantHandleLog, storagePoolVO.getPoolType()); return StrategyPriority.CANT_HANDLE; } List snapshots = snapshotDao.listByVolumeIdAndTypeNotInAndStateNotRemoved(volume.getId(), Snapshot.Type.GROUP); 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 5c009bc587d2..f1f120101a1f 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 @@ -83,6 +83,7 @@ import org.apache.cloudstack.storage.to.TemplateObjectTO; import org.apache.cloudstack.storage.to.VolumeObjectTO; import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.LogManager; @@ -1342,13 +1343,9 @@ private void createManagedVolumeCopyTemplateAsync(VolumeInfo volumeInfo, Primary grantAccess(volumeInfo, destHost, primaryDataStore); volumeInfo = volFactory.getVolume(volumeInfo.getId(), primaryDataStore); // For Netapp ONTAP iscsiName or Lun path is available only after grantAccess - String managedStoreTarget = volumeInfo.get_iScsiName() != null ? volumeInfo.get_iScsiName() : volumeInfo.getUuid(); + String managedStoreTarget = ObjectUtils.defaultIfNull(volumeInfo.get_iScsiName(), volumeInfo.getUuid()); details.put(PrimaryDataStore.MANAGED_STORE_TARGET, managedStoreTarget); primaryDataStore.setDetails(details); - // Update destTemplateInfo with the iSCSI path from volumeInfo - if (destTemplateInfo instanceof TemplateObject) { - ((TemplateObject)destTemplateInfo).setInstallPath(volumeInfo.getPath()); - } try { motionSrv.copyAsync(srcTemplateInfo, destTemplateInfo, destHost, caller); 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 aa0088064ca7..495ae45a2896 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 @@ -16,6 +16,9 @@ // under the License. package com.cloud.hypervisor.kvm.storage; +import java.io.File; +import java.io.FileWriter; +import java.nio.file.Path; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -97,19 +100,8 @@ public boolean connectPhysicalDisk(String volumeUuid, KVMStoragePool pool, Map 0) { @@ -283,8 +294,9 @@ private long getDeviceSize(String deviceByPath) { logger.debug("Device by-path does not exist yet: " + deviceByPath); return 0L; } - } catch (Exception ignore) { + } catch (Exception ex) { // If FS check fails for any reason, fall back to blockdev call + logger.error("Error fetching device size for {}", deviceByPath, ex); } Script iScsiAdmCmd = new Script(true, "blockdev", 0, logger); @@ -340,17 +352,20 @@ private String getComponent(String path, int index) { */ private boolean hasOtherActiveLuns(String host, int port, String iqn, String lun) { String prefix = "ip-" + host + ":" + port + "-iscsi-" + iqn + "-lun-"; - java.io.File byPathDir = new java.io.File("/dev/disk/by-path"); + File byPathDir = new File("/dev/disk/by-path"); if (!byPathDir.exists() || !byPathDir.isDirectory()) { return false; } - java.io.File[] entries = byPathDir.listFiles(); + File[] entries = byPathDir.listFiles(); if (entries == null) { return false; } - for (java.io.File entry : entries) { + for (File entry : entries) { String name = entry.getName(); - if (name.startsWith(prefix) && !name.equals(prefix + lun)) { + // Skip partition entries (e.g. lun-0-part1, lun-0-part2) — these are not + // independent LUNs, they are partition symlinks for the same LUN disk. + // Only count actual LUN entries (no "-part" suffix after the lun number). + if (name.startsWith(prefix) && !name.equals(prefix + lun) && !name.contains("-part")) { logger.debug("Found other active LUN on same target: " + name); return true; } @@ -358,6 +373,45 @@ private boolean hasOtherActiveLuns(String host, int port, String iqn, String lun return false; } + /** + * Removes a single stale SCSI device from the kernel using the sysfs interface. + * + * When ONTAP unmaps a LUN from the host's igroup, the by-path symlink and the + * underlying SCSI device (/dev/sdX) remain present in the kernel until explicitly + * removed — the kernel does not auto-remove devices from live iSCSI sessions. + * + * This method resolves the by-path symlink to the real block device name (e.g. sdd), + * then writes "1" to /sys/block//device/delete — the standard Linux kernel SCSI + * API for removing a single device without tearing down the entire iSCSI session. + * Once the kernel processes the delete, it also removes the by-path symlink. + * + * This is used instead of iscsiadm --logout when other LUNs on the same IQN are still + * active (ONTAP single-IQN-per-SVM model), since logout would tear down ALL LUNs. + */ + private void removeStaleScsiDevice(String host, int port, String iqn, String lun) { + String byPath = getByPath(host, port, "/" + iqn + "/" + lun); + Path byPathLink = Paths.get(byPath); + if (!Files.exists(byPathLink)) { + logger.debug("by-path entry for LUN " + lun + " already gone, nothing to remove"); + return; + } + try { + Path realDevice = byPathLink.toRealPath(); + String devName = realDevice.getFileName().toString(); + File deleteFile = new File("/sys/block/" + devName + "/device/delete"); + if (!deleteFile.exists()) { + logger.warn("sysfs delete entry not found for device " + devName + " — cannot remove stale SCSI device"); + return; + } + try (FileWriter fw = new FileWriter(deleteFile)) { + fw.write("1"); + } + logger.info("Removed stale SCSI device " + devName + " for LUN /" + iqn + "/" + lun + " via sysfs"); + } catch (Exception e) { + logger.warn("Failed to remove stale SCSI device for LUN /" + iqn + "/" + lun + ": " + e.getMessage()); + } + } + private boolean disconnectPhysicalDisk(String host, int port, String iqn, String lun) { // Check if other LUNs on the same IQN target are still in use. // ONTAP (and similar) uses a single IQN per SVM with multiple LUNs. @@ -365,8 +419,15 @@ private boolean disconnectPhysicalDisk(String host, int port, String iqn, String // which would destroy access to ALL LUNs — not just the one being disconnected. if (hasOtherActiveLuns(host, port, iqn, lun)) { logger.info("Skipping iSCSI logout for /" + iqn + "/" + lun + - " — other LUNs on the same target are still active"); - return true; + " — other LUNs on the same target are still active. Removing stale SCSI device for this LUN only."); + removeStaleScsiDevice(host, port, iqn, lun); + // After removing this LUN's device, re-check: if no other LUNs remain active, + // If it is the last one then must logout to clean up the iSCSI session entirely. + if (hasOtherActiveLuns(host, port, iqn, lun)) { + logger.info("Other LUNs still active after removing /" + iqn + "/" + lun + " — session kept alive."); + return true; + } + logger.info("No more active LUNs on target after removing /" + iqn + "/" + lun + " — proceeding with iSCSI logout."); } // No other LUNs active on this target — safe to logout and delete the node record. diff --git a/plugins/storage/volume/ontap/pom.xml b/plugins/storage/volume/ontap/pom.xml index 94ca574e1788..12035f01d7f9 100644 --- a/plugins/storage/volume/ontap/pom.xml +++ b/plugins/storage/volume/ontap/pom.xml @@ -137,7 +137,7 @@ org.apache.cloudstack cloud-engine-storage-snapshot - 4.23.0.0-SNAPSHOT + ${project.version} compile 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 8a47c93ab718..04996b74d2a5 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 @@ -29,6 +29,7 @@ import com.cloud.storage.Storage; import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeDetailVO; import com.cloud.storage.VolumeVO; import com.cloud.storage.ScopeType; import com.cloud.storage.dao.SnapshotDetailsDao; @@ -53,7 +54,6 @@ 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.client.SnapshotFeignClient; import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot; import org.apache.cloudstack.storage.feign.model.Lun; @@ -70,6 +70,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import javax.inject.Inject; import java.util.ArrayList; @@ -109,8 +110,8 @@ public DataTO getTO(DataObject data) { public DataStoreTO getStoreTO(DataStore store) { return null; } @Override - public boolean volumesRequireGrantAccessWhenUsed(){ - logger.info("OntapPrimaryDatastoreDriver: volumesRequireGrantAccessWhenUsed: Called"); + public boolean volumesRequireGrantAccessWhenUsed() { + logger.trace("volumesRequireGrantAccessWhenUsed invoked"); return true; } @@ -141,19 +142,18 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet logger.error("createAsync: Storage Pool not found for id: " + dataStore.getId()); throw new CloudRuntimeException("Storage Pool not found for id: " + dataStore.getId()); } - String storagePoolUuid = dataStore.getUuid(); Map details = storagePoolDetailsDao.listDetailsKeyPairs(dataStore.getId()); if (dataObject.getType() == DataObjectType.VOLUME) { VolumeInfo volInfo = (VolumeInfo) dataObject; - // Create the backend storage object (LUN for iSCSI, no-op for NFS) - CloudStackVolume created = createCloudStackVolume(dataStore, volInfo, details); - // Update CloudStack volume record with storage pool association and protocol-specific details VolumeVO volumeVO = volumeDao.findById(volInfo.getId()); if (volumeVO != null) { + // Create the backend storage object (LUN for iSCSI, no-op for NFS) + CloudStackVolume created = createCloudStackVolume(storagePool, volInfo, details); + volumeVO.setPoolType(storagePool.getPoolType()); volumeVO.setPoolId(storagePool.getId()); @@ -201,22 +201,10 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet /** * Creates a volume on the ONTAP backend. */ - private CloudStackVolume createCloudStackVolume(DataStore dataStore, DataObject dataObject, Map details) { - StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); - if (storagePool == null) { - logger.error("createCloudStackVolume: Storage Pool not found for id: {}", dataStore.getId()); - throw new CloudRuntimeException("Storage Pool not found for id: " + dataStore.getId()); - } - + private CloudStackVolume createCloudStackVolume(StoragePoolVO storagePool, VolumeInfo volumeObject, Map details) { StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details); - - if (dataObject.getType() == DataObjectType.VOLUME) { - VolumeInfo volumeObject = (VolumeInfo) dataObject; - CloudStackVolume cloudStackVolumeRequest = OntapStorageUtils.createCloudStackVolumeRequestByProtocol(storagePool, details, volumeObject); - return storageStrategy.createCloudStackVolume(cloudStackVolumeRequest); - } else { - throw new CloudRuntimeException("Unsupported DataObjectType: " + dataObject.getType()); - } + CloudStackVolume cloudStackVolumeRequest = OntapStorageUtils.createCloudStackVolumeRequestByProtocol(storagePool, details, volumeObject); + return storageStrategy.createCloudStackVolume(cloudStackVolumeRequest); } /** @@ -243,7 +231,7 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details); logger.info("createCloudStackVolumeForTypeVolume: Connection to Ontap SVM [{}] successful, preparing CloudStackVolumeRequest", details.get(OntapStorageConstants.SVM_NAME)); VolumeInfo volumeInfo = (VolumeInfo) data; - CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool,details,volumeInfo); + CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool, details, volumeInfo); storageStrategy.deleteCloudStackVolume(cloudStackVolumeRequest); logger.info("deleteAsync: Volume deleted: " + volumeInfo.getId()); commandResult.setResult(null); @@ -308,7 +296,7 @@ private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult comman if (jobResponse != null && jobResponse.getJob() != null) { // Poll for job completion - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2); + Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); if (!jobSucceeded) { throw new CloudRuntimeException("Delete job failed for snapshot [" + snapshotName + "] on FlexVol [" + flexVolUuid + "]"); @@ -383,7 +371,6 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore logger.error("grantAccess: Storage Pool not found for id: " + dataStore.getId()); throw new CloudRuntimeException("Storage Pool not found for id: " + dataStore.getId()); } - String storagePoolUuid = dataStore.getUuid(); // ONTAP managed storage only supports cluster and zone scoped pools if (storagePool.getScope() != ScopeType.CLUSTER && storagePool.getScope() != ScopeType.ZONE) { @@ -403,46 +390,7 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { // Only retrieve LUN name for iSCSI volumes - String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME).getValue(); - UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details); - String accessGroupName = OntapStorageUtils.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( - OntapStorageConstants.NAME, accessGroupName, - OntapStorageConstants.SVM_DOT_NAME, svmName - ); - Igroup igroup = new Igroup(); - AccessGroup accessGroup = sanStrategy.getAccessGroup(getAccessGroupMap); - if(accessGroup == null || accessGroup.getIgroup() == null) { - logger.info("grantAccess: Igroup {} does not exist for the host {} : Need to create Igroup for the host ", accessGroupName, 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{ - logger.info("grantAccess: Igroup {} already exist for the host {}: ", accessGroup.getIgroup().getName() ,host.getName()); - igroup = accessGroup.getIgroup(); - /* TODO Below cases will be covered later, for now they will be a pre-requisite on customer side - 1. Igroup exist with the same name but host initiator has been rempved - 2. Igroup exist with the same name but host initiator has been changed may be due to new NIC or new adapter - In both cases we need to verify current host initiator is registered in the igroup before allowing access - Incase it is not , add it and proceed for lun-mapping - */ - } - logger.info("grantAccess: Igroup {} is present now with initiators {} ", accessGroup.getIgroup().getName(), accessGroup.getIgroup().getInitiators()); - // Create or retrieve existing LUN mapping - String lunNumber = sanStrategy.ensureLunMapped(svmName, cloudStackVolumeName, accessGroupName); - - // Update volume path if changed (e.g., after migration or re-mapping) - String iscsiPath = OntapStorageConstants.SLASH + storagePool.getPath() + OntapStorageConstants.SLASH + lunNumber; - if (volumeVO.getPath() == null || !volumeVO.getPath().equals(iscsiPath)) { - volumeVO.set_iScsiName(iscsiPath); - volumeVO.setPath(iscsiPath); - } + grantAccessIscsi(host, volumeVO, details, svmName, storagePool); } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { // For NFS, no access grant needed - file is accessible via mount logger.debug("grantAccess: NFS volume [{}], no igroup mapping required", volumeVO.getUuid()); @@ -462,6 +410,47 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore } } + private void grantAccessIscsi(Host host, VolumeVO volumeVO, Map details, String svmName, StoragePoolVO storagePool) { + String cloudStackVolumeName = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME).getValue(); + UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details); + String accessGroupName = OntapStorageUtils.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( + OntapStorageConstants.NAME, accessGroupName, + OntapStorageConstants.SVM_DOT_NAME, svmName + ); + AccessGroup accessGroup = sanStrategy.getAccessGroup(getAccessGroupMap); + if(accessGroup == null || accessGroup.getIgroup() == null) { + logger.info("grantAccess: Igroup {} does not exist for the host {} : Need to create Igroup for the host ", accessGroupName, 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{ + logger.info("grantAccess: Igroup {} already exist for the host {}: ", accessGroup.getIgroup().getName() , host.getName()); + /* TODO Below cases will be covered later, for now they will be a pre-requisite on customer side + 1. Igroup exist with the same name but host initiator has been removed + 2. Igroup exist with the same name but host initiator has been changed may be due to new NIC or new adapter + In both cases we need to verify current host initiator is registered in the igroup before allowing access + Incase it is not , add it and proceed for lun-mapping + */ + } + logger.info("grantAccess: Igroup {} is present now with initiators {} ", accessGroup.getIgroup().getName(), accessGroup.getIgroup().getInitiators()); + // Create or retrieve existing LUN mapping + String lunNumber = sanStrategy.ensureLunMapped(svmName, cloudStackVolumeName, accessGroupName); + + // Update volume path if changed (e.g., after migration or re-mapping) + String iscsiPath = OntapStorageConstants.SLASH + storagePool.getPath() + OntapStorageConstants.SLASH + lunNumber; + if (volumeVO.getPath() == null || !volumeVO.getPath().equals(iscsiPath)) { + volumeVO.set_iScsiName(iscsiPath); + volumeVO.setPath(iscsiPath); + } + } + /** * Revokes a host's access to a volume. */ @@ -520,43 +509,62 @@ private void revokeAccessForVolume(StoragePoolVO storagePool, VolumeVO volumeVO, String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getName()); // Retrieve LUN name from volume details; if missing, volume may not have been fully created - String lunName = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME) != null ? - volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME).getValue() : null; - if (lunName == null) { - logger.warn("revokeAccessForVolume: No LUN name found for volume [{}]; skipping revoke", volumeVO.getId()); - return; - } - - // Verify LUN still exists on ONTAP (may have been manually deleted) - CloudStackVolume cloudStackVolume = getCloudStackVolumeByName(storageStrategy, svmName, lunName); - if (cloudStackVolume == null || cloudStackVolume.getLun() == null || cloudStackVolume.getLun().getUuid() == null) { - logger.warn("revokeAccessForVolume: LUN for volume [{}] not found on ONTAP, skipping revoke", volumeVO.getId()); - return; - } - - // Verify igroup still exists on ONTAP - AccessGroup accessGroup = getAccessGroupByName(storageStrategy, svmName, accessGroupName); - if (accessGroup == null || accessGroup.getIgroup() == null || accessGroup.getIgroup().getUuid() == null) { - logger.warn("revokeAccessForVolume: iGroup [{}] not found on ONTAP, skipping revoke", accessGroupName); - return; - } - - // Verify host initiator is in the igroup before attempting to remove mapping - SANStrategy sanStrategy = (UnifiedSANStrategy) storageStrategy; - if (!sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroup.getIgroup())) { - logger.warn("revokeAccessForVolume: Initiator [{}] is not in iGroup [{}], skipping revoke", - host.getStorageUrl(), accessGroupName); - return; - } + VolumeDetailVO lunDetail = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME); + ValidateRevoke result = getValidateRevoke(volumeVO, host, lunDetail, storageStrategy, svmName, accessGroupName); + if (result == null) return; // Remove the LUN mapping from the igroup Map disableLogicalAccessMap = new HashMap<>(); - disableLogicalAccessMap.put(OntapStorageConstants.LUN_DOT_UUID, cloudStackVolume.getLun().getUuid()); - disableLogicalAccessMap.put(OntapStorageConstants.IGROUP_DOT_UUID, accessGroup.getIgroup().getUuid()); + disableLogicalAccessMap.put(OntapStorageConstants.LUN_DOT_UUID, result.cloudStackVolume.getLun().getUuid()); + disableLogicalAccessMap.put(OntapStorageConstants.IGROUP_DOT_UUID, result.accessGroup.getIgroup().getUuid()); storageStrategy.disableLogicalAccess(disableLogicalAccessMap); logger.info("revokeAccessForVolume: Successfully revoked access to LUN [{}] for host [{}]", - lunName, host.getName()); + result.lunName, host.getName()); + } + } + + @Nullable + private ValidateRevoke getValidateRevoke(VolumeVO volumeVO, Host host, VolumeDetailVO lunDetail, StorageStrategy storageStrategy, String svmName, String accessGroupName) { + String lunName = lunDetail != null ? lunDetail.getValue() : null; + if (lunName == null) { + logger.warn("revokeAccessForVolume: No LUN name found for volume [{}]; skipping revoke", volumeVO.getId()); + return null; + } + + // Verify LUN still exists on ONTAP (may have been manually deleted) + CloudStackVolume cloudStackVolume = getCloudStackVolumeByName(storageStrategy, svmName, lunName); + if (cloudStackVolume == null || cloudStackVolume.getLun() == null || cloudStackVolume.getLun().getUuid() == null) { + logger.warn("revokeAccessForVolume: LUN for volume [{}] not found on ONTAP, skipping revoke", volumeVO.getId()); + return null; + } + + // Verify igroup still exists on ONTAP + AccessGroup accessGroup = getAccessGroupByName(storageStrategy, svmName, accessGroupName); + if (accessGroup == null || accessGroup.getIgroup() == null || accessGroup.getIgroup().getUuid() == null) { + logger.warn("revokeAccessForVolume: iGroup [{}] not found on ONTAP, skipping revoke", accessGroupName); + return null; + } + + // Verify host initiator is in the igroup before attempting to remove mapping + SANStrategy sanStrategy = (UnifiedSANStrategy) storageStrategy; + if (!sanStrategy.validateInitiatorInAccessGroup(host.getStorageUrl(), svmName, accessGroup.getIgroup())) { + logger.warn("revokeAccessForVolume: Initiator [{}] is not in iGroup [{}], skipping revoke", + host.getStorageUrl(), accessGroupName); + return null; + } + return new ValidateRevoke(lunName, cloudStackVolume, accessGroup); + } + + private static class ValidateRevoke { + public final String lunName; + public final CloudStackVolume cloudStackVolume; + public final AccessGroup accessGroup; + + public ValidateRevoke(String lunName, CloudStackVolume cloudStackVolume, AccessGroup accessGroup) { + this.lunName = lunName; + this.cloudStackVolume = cloudStackVolume; + this.accessGroup = accessGroup; } } @@ -671,10 +679,9 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback nfs) { + this.nfsEnabled = nfs != null ? Boolean.TRUE.equals(nfs.get("enabled")) : false; + } + public Boolean getIscsiEnabled() { - return iscsiEnabled; + return Boolean.TRUE.equals(iscsiEnabled); } public void setIscsiEnabled(Boolean iscsiEnabled) { this.iscsiEnabled = iscsiEnabled; } + @JsonSetter("iscsi") + public void setIscsi(Map iscsi) { + this.iscsiEnabled = iscsi != null ? Boolean.TRUE.equals(iscsi.get("enabled")) : false; + } + public Boolean getFcpEnabled() { - return fcpEnabled; + return Boolean.TRUE.equals(fcpEnabled); } public void setFcpEnabled(Boolean fcpEnabled) { this.fcpEnabled = fcpEnabled; } - public Boolean getNfsEnabled() { - return nfsEnabled; - } - - public void setNfsEnabled(Boolean nfsEnabled) { - this.nfsEnabled = nfsEnabled; + @JsonSetter("fcp") + public void setFcp(Map fcp) { + this.fcpEnabled = fcp != null ? Boolean.TRUE.equals(fcp.get("enabled")) : false; } public List getAggregates() { diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java index eaa5b2ed2ae9..82cea1260e10 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java @@ -28,16 +28,23 @@ public class VolumeConcise { @JsonProperty("uuid") private String uuid; + @JsonProperty("name") private String name; + public String getUuid() { return uuid; } + public void setUuid(String uuid) { this.uuid = uuid; } + public String getName() { return name; } - public void setName(String name) {} + + public void setName(String name) { + this.name = name; + } } 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 b055dad425a8..d7b89de25461 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 @@ -19,7 +19,7 @@ package org.apache.cloudstack.storage.lifecycle; - +import org.apache.cloudstack.engine.subsystem.api.storage.Scope; import com.cloud.agent.api.StoragePoolInfo; import com.cloud.dc.ClusterVO; import com.cloud.dc.dao.ClusterDao; @@ -33,6 +33,7 @@ import com.cloud.storage.StoragePoolAutomation; import com.cloud.utils.exception.CloudRuntimeException; import com.google.common.base.Preconditions; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.engine.subsystem.api.storage.ClusterScope; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.HostScope; @@ -54,11 +55,13 @@ import org.apache.cloudstack.storage.utils.OntapStorageConstants; import org.apache.cloudstack.storage.utils.OntapStorageUtils; import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.inject.Inject; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -167,7 +170,7 @@ public DataStore initialize(Map dsInfos) { path = OntapStorageConstants.SLASH + storagePoolName; port = OntapStorageConstants.NFS3_PORT; // Force NFSv3 for ONTAP managed storage to avoid NFSv4 ID mapping issues - details.put(OntapStorageConstants.NFS_MOUNT_OPTIONS, OntapStorageConstants.NFS3_MOUNT_OPTIONS_VER_3); + details.put(ApiConstants.NFS_MOUNT_OPTIONS, OntapStorageConstants.NFS3_MOUNT_OPTIONS_VER_3); logger.info("Setting NFS path for storage pool: " + path + ", port: " + port + " with mount option: vers=3"); break; case ISCSI: @@ -216,19 +219,19 @@ private long validateInitializeInputs(Long capacityBytes, Long podId, Long clust throw new CloudRuntimeException("Cluster Id or Pod Id is null, cannot create primary storage"); } + if (podId == null && zoneId == null) { + throw new CloudRuntimeException("Pod Id, Cluster Id and Zone Id are all null, cannot create primary storage"); + } + if (podId == null) { - if (zoneId != null) { - logger.info("Both Pod Id and Cluster Id are null, Primary storage pool will be associated with a Zone"); - } else { - throw new CloudRuntimeException("Pod Id, Cluster Id and Zone Id are all null, cannot create primary storage"); - } + logger.info("Both Pod Id and Cluster Id are null, Primary storage pool will be associated with a Zone"); } - if (storagePoolName == null || storagePoolName.isEmpty()) { + if (StringUtils.isBlank(storagePoolName)) { throw new CloudRuntimeException("Storage pool name is null or empty, cannot create primary storage"); } - if (providerName == null || providerName.isEmpty()) { + if (StringUtils.isBlank(providerName)) { throw new CloudRuntimeException("Provider name is null or empty, cannot create primary storage"); } @@ -263,15 +266,15 @@ private long validateInitializeInputs(Long capacityBytes, Long podId, Long clust if (!requiredKeys.contains(key)) { throw new CloudRuntimeException("Unexpected ONTAP detail key in URL: " + key); } - if (val == null || val.isEmpty()) { + if (StringUtils.isBlank(val)) { throw new CloudRuntimeException("ONTAP primary storage creation failed, empty detail: " + key); } } // Detect missing required keys - Set providedKeys = new java.util.HashSet<>(details.keySet()); + Set providedKeys = new HashSet<>(details.keySet()); if (!providedKeys.containsAll(requiredKeys)) { - Set missing = new java.util.HashSet<>(requiredKeys); + Set missing = new HashSet<>(requiredKeys); missing.removeAll(providedKeys); throw new CloudRuntimeException("ONTAP primary storage creation failed, missing detail(s): " + missing); } @@ -283,16 +286,16 @@ private long validateInitializeInputs(Long capacityBytes, Long podId, Long clust public boolean attachCluster(DataStore dataStore, ClusterScope scope) { logger.debug("In attachCluster for ONTAP primary storage"); if (dataStore == null) { - throw new InvalidParameterValueException(" dataStore should not be null"); + throw new InvalidParameterValueException("DataStore should not be null"); } if (scope == null) { - throw new InvalidParameterValueException(" scope should not be null"); + throw new InvalidParameterValueException("Scope should not be null"); } List hostsIdentifier = new ArrayList<>(); StoragePoolVO storagePool = storagePoolDao.findById(dataStore.getId()); if (storagePool == null) { logger.error("attachCluster : Storage Pool not found for id: " + dataStore.getId()); - throw new CloudRuntimeException(" Storage Pool not found for id: " + dataStore.getId()); + throw new CloudRuntimeException("Storage Pool not found for id: " + dataStore.getId()); } PrimaryDataStoreInfo primaryStore = (PrimaryDataStoreInfo)dataStore; List hostsToConnect = _resourceMgr.getEligibleUpAndEnabledHostsInClusterForStorageConnection(primaryStore); @@ -309,21 +312,7 @@ public boolean attachCluster(DataStore dataStore, ClusterScope scope) { } logger.debug("attachCluster: Attaching the pool to each of the host in the cluster: {}", primaryStore.getClusterId()); // We need to create export policy at pool level and igroup at host level(in grantAccess) - if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { - // If there are no eligible host, export policy or igroup will not be created and will be taken as part of HostListener - if (!hostsIdentifier.isEmpty()) { - try { - AccessGroup accessGroupRequest = new AccessGroup(); - accessGroupRequest.setHostsToConnect(hostsToConnect); - accessGroupRequest.setScope(scope); - accessGroupRequest.setStoragePoolId(storagePool.getId()); - strategy.createAccessGroup(accessGroupRequest); - } catch (Exception e) { - logger.error("attachCluster: Failed to create access group on storage system for cluster: " + primaryStore.getClusterId() + ". Exception: " + e.getMessage()); - throw new CloudRuntimeException("Failed to create access group on storage system for cluster: " + primaryStore.getClusterId() + ". Exception: " + e.getMessage()); - } - } - } + createNfsAccessGroupIfNeeded(details, hostsIdentifier, hostsToConnect, scope, storagePool, strategy); logger.debug("attachCluster: Attaching the pool to each of the host in the cluster: {}", primaryStore.getClusterId()); for (HostVO host : hostsToConnect) { @@ -375,21 +364,8 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper } // We need to create export policy at pool level and igroup at host level - if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { - // If there are no eligible host, export policy or igroup will not be created and will be taken as part of HostListener - if (!hostsIdentifier.isEmpty()) { - try { - AccessGroup accessGroupRequest = new AccessGroup(); - accessGroupRequest.setHostsToConnect(hostsToConnect); - accessGroupRequest.setScope(scope); - accessGroupRequest.setStoragePoolId(storagePool.getId()); - strategy.createAccessGroup(accessGroupRequest); - } catch (Exception e) { - logger.error("attachZone: Failed to create access group on storage system for zone with Exception: " + e.getMessage()); - throw new CloudRuntimeException(" Failed to create access group on storage system for zone with Exception: " + e.getMessage()); - } - } - } + createNfsAccessGroupIfNeeded(details, hostsIdentifier, hostsToConnect, scope, storagePool, strategy); + for (HostVO host : hostsToConnect) { try { _storageMgr.connectHostToSharedPool(host, dataStore.getId()); @@ -420,7 +396,7 @@ private boolean validateProtocolSupportAndFetchHostsIdentifier(List host for (HostVO host : hosts) { if (host != null) { ip = host.getStorageIpAddress() != null ? host.getStorageIpAddress().trim() : ""; - if (ip.isEmpty() && host.getPrivateIpAddress() != null || host.getPrivateIpAddress().trim().isEmpty()) { + if (ip.isEmpty() && StringUtils.isBlank(host.getPrivateIpAddress() )) { // TODO we will inform customer through alert for excluded host because of protocol enabled on host continue; } else { @@ -437,6 +413,32 @@ private boolean validateProtocolSupportAndFetchHostsIdentifier(List host return true; } + /** + * Creates an NFS export policy (access group) on the ONTAP storage if the protocol is NFS3 + * and there are eligible hosts. Skipped for iSCSI (igroups are created per-host in grantAccess). + */ + private void createNfsAccessGroupIfNeeded(Map details, List hostsIdentifier, + List hostsToConnect, Scope scope, + StoragePoolVO storagePool, StorageStrategy strategy) { + if (!ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) { + return; + } + if (hostsIdentifier.isEmpty()) { + // No eligible hosts — export policy will be created later via HostListener when hosts come up + return; + } + try { + AccessGroup accessGroupRequest = new AccessGroup(); + accessGroupRequest.setHostsToConnect(hostsToConnect); + accessGroupRequest.setScope(scope); + accessGroupRequest.setStoragePoolId(storagePool.getId()); + strategy.createAccessGroup(accessGroupRequest); + } catch (Exception e) { + logger.error("Failed to create NFS access group on storage for pool {}: {}", storagePool.getName(), e.getMessage()); + throw new CloudRuntimeException("Failed to create NFS access group on storage for pool " + storagePool.getName() + ": " + e.getMessage()); + } + } + @Override public boolean maintain(DataStore store) { logger.info("Placing storage pool {} in maintenance mode", store); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java index fd527d285285..ecdd3efd2c5c 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/listener/OntapHostListener.java @@ -25,6 +25,7 @@ import com.cloud.agent.api.ModifyStoragePoolAnswer; import com.cloud.agent.api.StoragePoolInfo; import com.cloud.alert.AlertManager; +import com.cloud.hypervisor.Hypervisor; import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.dao.StoragePoolHostDao; import org.apache.logging.log4j.Logger; @@ -68,8 +69,11 @@ public boolean hostConnect(long hostId, long poolId) { logger.error("host was not found with id : {}", hostId); return false; } + if (!host.getHypervisorType().equals(Hypervisor.HypervisorType.KVM)) { + logger.error("ONTAP plugin does not support {} type host currently ", host.getHypervisorType()); + return false; + } - // TODO add host type check also since we support only KVM for now, host.getHypervisorType().equals(HypervisorType.KVM) StoragePool pool = _storagePoolDao.findById(poolId); if (pool == null) { logger.error("Failed to connect host - storage pool not found with id: {}", poolId); @@ -100,6 +104,13 @@ public boolean hostConnect(long hostId, long poolId) { } // Get the mount path from the answer + + if (!(answer instanceof ModifyStoragePoolAnswer)) { + throw new CloudRuntimeException(String.format( + "Unexpected answer type %s returned for modify storage pool command for pool %s on host %d", + answer.getClass().getName(), pool, hostId)); + } + ModifyStoragePoolAnswer mspAnswer = (ModifyStoragePoolAnswer) answer; StoragePoolInfo poolInfo = mspAnswer.getPoolInfo(); if (poolInfo == null) { @@ -141,41 +152,39 @@ public boolean hostConnect(long hostId, long poolId) { } @Override - public boolean hostDisconnected(Host host, StoragePool pool) { - logger.info("Disconnect from host " + host.getId() + " from pool " + pool.getName()); + public boolean hostDisconnected(long hostId, long poolId) { + logger.info("Disconnect from host " + hostId + " from pool " + poolId); - Host hostToremove = _hostDao.findById(host.getId()); + Host hostToremove = _hostDao.findById(hostId); if (hostToremove == null) { - logger.error("Failed to add host by HostListener as host was not found with id : {}", host.getId()); + logger.error("Failed to add host by HostListener as host was not found with id : {}", hostId); + return false; + } + + StoragePool pool = _storagePoolDao.findById(poolId); + if (pool == null) { + logger.error("Failed to disconnect host - storage pool not found with id: {}", poolId); return false; } - // TODO add storage pool get validation - logger.info("Disconnecting host {} from ONTAP storage pool {}", host.getName(), pool.getName()); + logger.info("Disconnecting host {} from ONTAP storage pool {}", hostToremove.getName(), pool.getName()); try { DeleteStoragePoolCommand cmd = new DeleteStoragePoolCommand(pool); - long hostId = host.getId(); Answer answer = _agentMgr.easySend(hostId, cmd); - if (answer != null && answer.getResult()) { - logger.info("Successfully disconnected host {} from ONTAP storage pool {}", host.getName(), pool.getName()); + logger.info("Successfully disconnected host {} from ONTAP storage pool {}", hostToremove.getName(), pool.getName()); return true; } else { String errMsg = (answer != null) ? answer.getDetails() : "Unknown error"; - logger.warn("Failed to disconnect host {} from storage pool {}. Error: {}", host.getName(), pool.getName(), errMsg); + logger.warn("Failed to disconnect host {} from storage pool {}. Error: {}", hostToremove.getName(), pool.getName(), errMsg); return false; } } catch (Exception e) { - logger.error("Exception while disconnecting host {} from storage pool {}", host.getName(), pool.getName(), e); + logger.error("Exception while disconnecting host {} from storage pool {}", hostToremove.getName(), pool.getName(), e); return false; } } - @Override - public boolean hostDisconnected(long hostId, long poolId) { - return false; - } - @Override public boolean hostAboutToBeRemoved(long hostId) { return false; 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 65d9e9622f04..1131cd1405c7 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 @@ -21,6 +21,7 @@ import com.cloud.utils.component.ComponentContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle; import org.apache.cloudstack.engine.subsystem.api.storage.HypervisorHostListener; @@ -28,7 +29,6 @@ import org.apache.cloudstack.storage.driver.OntapPrimaryDatastoreDriver; import org.apache.cloudstack.storage.lifecycle.OntapPrimaryDatastoreLifecycle; import org.apache.cloudstack.storage.listener.OntapHostListener; -import org.apache.cloudstack.storage.utils.OntapStorageConstants; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Component; @@ -66,7 +66,7 @@ public HypervisorHostListener getHostListener() { @Override public String getName() { logger.trace("OntapPrimaryDatastoreProvider: getName: Called"); - return OntapStorageConstants.ONTAP_PLUGIN_NAME; + return DataStoreProvider.ONTAP_PLUGIN_NAME; } @Override diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java index cb9ac6f61bcc..935e1284de42 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/provider/StorageProviderFactory.java @@ -51,12 +51,10 @@ public static StorageStrategy getStrategy(OntapStorage ontapStorage) { case NFS3: UnifiedNASStrategy unifiedNASStrategy = new UnifiedNASStrategy(ontapStorage); ComponentContext.inject(unifiedNASStrategy); - unifiedNASStrategy.setOntapStorage(ontapStorage); return unifiedNASStrategy; case ISCSI: UnifiedSANStrategy unifiedSANStrategy = new UnifiedSANStrategy(ontapStorage); ComponentContext.inject(unifiedSANStrategy); - unifiedSANStrategy.setOntapStorage(ontapStorage); return unifiedSANStrategy; default: throw new CloudRuntimeException("Unsupported protocol: " + protocol); 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 7d9dd33f7eff..bd808a26d6f8 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,31 +108,32 @@ public boolean connect() { Svm svm = new Svm(); logger.info("Fetching the SVM details..."); Map queryParams = Map.of(OntapStorageConstants.NAME, svmName, OntapStorageConstants.FIELDS, OntapStorageConstants.AGGREGATES + - OntapStorageConstants.COMMA + OntapStorageConstants.STATE); + OntapStorageConstants.COMMA + OntapStorageConstants.STATE + + OntapStorageConstants.COMMA + OntapStorageConstants.NFS_ENABLED + OntapStorageConstants.COMMA + OntapStorageConstants.ISCSI_ENABLED); OntapResponse svms = svmFeignClient.getSvmResponse(queryParams, authHeader); if (svms != null && svms.getRecords() != null && !svms.getRecords().isEmpty()) { svm = svms.getRecords().get(0); } else { - logger.error("No SVM found on the ONTAP cluster by the name" + svmName + "."); - return false; + logger.error("No SVM found on the ONTAP cluster by the name " + svmName + "."); + throw new CloudRuntimeException("No SVM found on the ONTAP cluster by the name " + svmName + "."); } logger.info("Validating SVM state and protocol settings..."); if (!Objects.equals(svm.getState(), OntapStorageConstants.RUNNING)) { logger.error("SVM " + svmName + " is not in running state."); - return false; + throw new CloudRuntimeException("SVM " + svmName + " is not in running state."); } - if (Objects.equals(storage.getProtocol(), OntapStorageConstants.NFS) && !svm.getNfsEnabled()) { + if (Objects.equals(storage.getProtocol(), ProtocolType.NFS3) && !svm.getNfsEnabled()) { logger.error("NFS protocol is not enabled on SVM " + svmName); - return false; - } else if (Objects.equals(storage.getProtocol(), OntapStorageConstants.ISCSI) && !svm.getIscsiEnabled()) { - logger.error("iSCSI protocol is not enabled on SVM " + svmName); - return false; + throw new CloudRuntimeException("NFS protocol is not enabled on SVM " + svmName); + } else if (Objects.equals(storage.getProtocol(), ProtocolType.ISCSI) && !svm.getIscsiEnabled()) { + logger.error("ISCSI protocol is not enabled on SVM " + svmName); + throw new CloudRuntimeException("ISCSI protocol is not enabled on SVM " + svmName); } List aggrs = svm.getAggregates(); if (aggrs == null || aggrs.isEmpty()) { logger.error("No aggregates are assigned to SVM " + svmName); - return false; + throw new CloudRuntimeException("No aggregates are assigned to SVM " + svmName); } for (Aggregate aggr : aggrs) { logger.debug("Found aggregate: " + aggr.getName() + " with UUID: " + aggr.getUuid()); @@ -155,13 +156,13 @@ public boolean connect() { } if (this.aggregates == null || this.aggregates.isEmpty()) { logger.error("No suitable aggregates found on SVM " + svmName + " for volume creation."); - return false; + throw new CloudRuntimeException("No suitable aggregates found on SVM " + svmName + " for volume creation."); } logger.info("Successfully connected to ONTAP cluster and validated ONTAP details provided"); } catch (Exception e) { logger.error("Failed to connect to ONTAP cluster: " + e.getMessage(), e); - return false; + throw new CloudRuntimeException("Failed to connect to ONTAP cluster: " + e.getMessage(), e); } return true; } @@ -257,7 +258,7 @@ public Volume createStorageVolume(String volumeName, Long size) { } String jobUUID = jobResponse.getJob().getUuid(); - Boolean jobSucceeded = jobPollForSuccess(jobUUID,10, 1); + Boolean jobSucceeded = jobPollForSuccess(jobUUID,10, 1000); if (!jobSucceeded) { logger.error("Volume creation job failed for volume: " + volumeName); throw new CloudRuntimeException("Volume creation job failed for volume: " + volumeName); @@ -339,9 +340,8 @@ public void deleteStorageVolume(Volume volume) { logger.info("Deleting ONTAP volume by name: " + volume.getName() + " and uuid: " + volume.getUuid()); String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); try { - // TODO: Implement lun and file deletion, if any, before deleting the volume JobResponse jobResponse = volumeFeignClient.deleteVolume(authHeader, volume.getUuid()); - Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(),10, 1); + Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(), 10, 1000); if (!jobSucceeded) { logger.error("Volume deletion job failed for volume: " + volume.getName()); throw new CloudRuntimeException("Volume deletion job failed for volume: " + volume.getName()); @@ -594,9 +594,9 @@ public abstract JobResponse revertSnapshotForCloudStackVolume(String snapshotNam * //TODO for NFS 3.0 and NFS 4.1 protocols (e.g., export rule management) * //TODO for Nvme/TCP and Nvme/FC protocols * @param values map including SVM name, LUN name, and igroup name (for SAN) or equivalent for NAS - * @return map containing logical unit number for the new/existing mapping (SAN) or relevant info for NAS + * @return String containing logical unit number for the new/existing mapping (SAN) or relevant info for NAS */ - abstract public Map enableLogicalAccess(Map values); + abstract public String enableLogicalAccess(Map values); /** * Method encapsulates the behavior based on the opted protocol in subclasses @@ -609,9 +609,9 @@ public abstract JobResponse revertSnapshotForCloudStackVolume(String snapshotNam * Method encapsulates the behavior based on the opted protocol in subclasses * lunMap lookup for iSCSI/FC protocols (GET-only, no side-effects) * @param values map with SVM name, LUN name, and igroup name (for SAN) or equivalent for NAS - * @return map containing logical unit number if mapping exists; otherwise null + * @return String containing logical unit number if mapping exists; otherwise null */ - abstract public Map getLogicalAccess(Map values); + abstract public String getLogicalAccess(Map values); // ── FlexVolume Snapshot accessors ──────────────────────────────────────── @@ -642,10 +642,10 @@ public String getAuthHeader() { * * @param jobUUID UUID of the ONTAP job to poll * @param maxRetries maximum number of poll attempts - * @param sleepTimeInSecs seconds to sleep between poll attempts + * @param sleepTimeInMilliSecs seconds to sleep between poll attempts * @return true if the job completed successfully */ - public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeInSecs) { + public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeInMilliSecs) { //Create URI for GET Job API int jobRetryCount = 0; Job jobResp = null; @@ -669,7 +669,7 @@ public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeIn } jobRetryCount++; - Thread.sleep(OntapStorageConstants.CREATE_VOLUME_CHECK_SLEEP_TIME); + Thread.sleep(sleepTimeInMilliSecs); } if (jobResp == null || !jobResp.getState().equals(OntapStorageConstants.JOB_SUCCESS)) { return false; 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 1b9af868f7dd..477e92630387 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 @@ -75,7 +75,7 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume logger.info("createCloudStackVolume: Create cloudstack volume " + cloudstackVolume); try { // Step 1: set cloudstack volume metadata - String volumeUuid = updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), cloudstackVolume.getVolumeInfo()); + updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), cloudstackVolume.getVolumeInfo()); // Step 2: Send command to KVM host to create qcow2 file using qemu-img Answer answer = createVolumeOnKVMHost(cloudstackVolume.getVolumeInfo()); if (answer == null || !answer.getResult()) { @@ -106,7 +106,7 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { logger.error("deleteCloudStackVolume: " + errMsg); throw new CloudRuntimeException(errMsg); } - }catch (Exception e) { + } catch (Exception e) { logger.error("deleteCloudStackVolume: error occured " + e); throw new CloudRuntimeException(e); } @@ -123,7 +123,7 @@ public CloudStackVolume getCloudStackVolume(Map cloudStackVolume CloudStackVolume cloudStackVolume = null; FileInfo fileInfo = getFile(cloudStackVolumeMap.get(OntapStorageConstants.VOLUME_UUID),cloudStackVolumeMap.get(OntapStorageConstants.FILE_PATH)); - if(fileInfo != null){ + if (fileInfo != null) { cloudStackVolume = new CloudStackVolume(); cloudStackVolume.setFlexVolumeUuid(cloudStackVolumeMap.get(OntapStorageConstants.VOLUME_UUID)); cloudStackVolume.setFile(fileInfo); @@ -202,7 +202,7 @@ public AccessGroup getAccessGroup(Map values) { } @Override - public Map enableLogicalAccess(Map values) { + public String enableLogicalAccess(Map values) { return null; } @@ -211,8 +211,8 @@ public void disableLogicalAccess(Map values) { } @Override - public Map getLogicalAccess(Map values) { - return Map.of(); + public String getLogicalAccess(Map values) { + return null; } private ExportPolicy createExportPolicy(String svmName, ExportPolicy policy) { @@ -298,75 +298,6 @@ private void assignExportPolicyToVolume(String volumeUuid, String policyName) { } } - private boolean createFile(String volumeUuid, String filePath, FileInfo fileInfo) { - logger.info("createFile: Creating file: {} in volume: {}", filePath, volumeUuid); - try { - String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); - nasFeignClient.createFile(authHeader, volumeUuid, filePath, fileInfo); - logger.info("createFile: File created successfully: {} in volume: {}", filePath, volumeUuid); - return true; - } catch (FeignException e) { - logger.error("createFile: Failed to create file: {} in volume: {}", filePath, volumeUuid, e); - return false; - } catch (Exception e) { - logger.error("createFile: Exception while creating file: {} in volume: {}", filePath, volumeUuid, e); - return false; - } - } - - private boolean deleteFile(String volumeUuid, String filePath) { - logger.info("deleteFile: Deleting file: {} from volume: {}", filePath, volumeUuid); - try { - String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); - nasFeignClient.deleteFile(authHeader, volumeUuid, filePath); - logger.info("deleteFile: File deleted successfully: {} from volume: {}", filePath, volumeUuid); - return true; - } catch (FeignException e) { - logger.error("deleteFile: Failed to delete file: {} from volume: {}", filePath, volumeUuid, e); - return false; - } catch (Exception e) { - logger.error("deleteFile: Exception while deleting file: {} from volume: {}", filePath, volumeUuid, e); - return false; - } - } - - private OntapResponse getFileInfo(String volumeUuid, String filePath) { - logger.debug("getFileInfo: Getting file info for: {} in volume: {}", filePath, volumeUuid); - try { - String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); - OntapResponse response = nasFeignClient.getFileResponse(authHeader, volumeUuid, filePath); - logger.debug("getFileInfo: Retrieved file info for: {} in volume: {}", filePath, volumeUuid); - return response; - } catch (FeignException e){ - if (e.status() == 404) { - logger.debug("getFileInfo: File not found: {} in volume: {}", filePath, volumeUuid); - return null; - } - logger.error("getFileInfo: Failed to get file info: {} in volume: {}", filePath, volumeUuid, e); - throw new CloudRuntimeException("Failed to get file info: " + e.getMessage()); - } catch (Exception e){ - logger.error("getFileInfo: Exception while getting file info: {} in volume: {}", filePath, volumeUuid, e); - throw new CloudRuntimeException("Failed to get file info: " + e.getMessage()); - } - } - - private boolean updateFile(String volumeUuid, String filePath, FileInfo fileInfo) { - logger.info("updateFile: Updating file: {} in volume: {}", filePath, volumeUuid); - try { - String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); - nasFeignClient.updateFile( authHeader, volumeUuid, filePath, fileInfo); - logger.info("updateFile: File updated successfully: {} in volume: {}", filePath, volumeUuid); - return true; - } catch (FeignException e) { - logger.error("updateFile: Failed to update file: {} in volume: {}", filePath, volumeUuid, e); - return false; - } catch (Exception e){ - logger.error("updateFile: Exception while updating file: {} in volume: {}", filePath, volumeUuid, e); - return false; - } - } - - private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String svmName , String volumeName){ String exportPolicyName = OntapStorageUtils.generateExportPolicyName(svmName,volumeName); @@ -405,25 +336,25 @@ private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String sv private String updateCloudStackVolumeMetadata(String dataStoreId, DataObject volumeInfo) { logger.info("updateCloudStackVolumeMetadata called with datastoreID: {} volumeInfo: {} ", dataStoreId, volumeInfo ); - try { - VolumeObject volumeObject = (VolumeObject) volumeInfo; - long volumeId = volumeObject.getId(); - logger.info("updateCloudStackVolumeMetadata: VolumeInfo ID from VolumeObject: {}", volumeId); - VolumeVO volume = volumeDao.findById(volumeId); - if (volume == null) { - throw new CloudRuntimeException("Volume not found with id: " + volumeId); - } - String volumeUuid = volumeInfo.getUuid(); - volume.setPoolType(Storage.StoragePoolType.NetworkFilesystem); - volume.setPoolId(Long.parseLong(dataStoreId)); - volume.setPath(volumeUuid); // Filename for qcow2 file - volumeDao.update(volume.getId(), volume); - logger.info("Updated volume path to {} for volume ID {}", volumeUuid, volumeId); - return volumeUuid; - }catch (Exception e){ - logger.error("updateCloudStackVolumeMetadata: Exception while updating volumeInfo: {} in volume: {}", dataStoreId, volumeInfo.getUuid(), e); - throw new CloudRuntimeException("Exception while updating volumeInfo: " + e.getMessage()); - } + try { + VolumeObject volumeObject = (VolumeObject) volumeInfo; + long volumeId = volumeObject.getId(); + logger.info("updateCloudStackVolumeMetadata: VolumeInfo ID from VolumeObject: {}", volumeId); + VolumeVO volume = volumeDao.findById(volumeId); + if (volume == null) { + throw new CloudRuntimeException("Volume not found with id: " + volumeId); + } + String volumeUuid = volumeInfo.getUuid(); + volume.setPoolType(Storage.StoragePoolType.NetworkFilesystem); + volume.setPoolId(Long.parseLong(dataStoreId)); + volume.setPath(volumeUuid); // Filename for qcow2 file + volumeDao.update(volume.getId(), volume); + logger.info("Updated volume path to {} for volume ID {}", volumeUuid, volumeId); + return volumeUuid; + } catch (Exception e){ + logger.error("updateCloudStackVolumeMetadata: Exception while updating volumeInfo: {} in volume: {}", dataStoreId, volumeInfo.getUuid(), e); + throw new CloudRuntimeException("Exception while updating volumeInfo: " + e.getMessage()); + } } private Answer createVolumeOnKVMHost(DataObject volumeInfo) { 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 a9664f4d4f24..5f1ac265fc50 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 @@ -37,6 +37,7 @@ import org.apache.cloudstack.storage.service.model.ProtocolType; import org.apache.cloudstack.storage.utils.OntapStorageConstants; import org.apache.cloudstack.storage.utils.OntapStorageUtils; +import org.apache.commons.collections.CollectionUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.inject.Inject; @@ -61,7 +62,7 @@ public void setOntapStorage(OntapStorage ontapStorage) { @Override public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume) { - logger.info("createCloudStackVolume : Creating Lun with cloudstackVolume request {} ", cloudstackVolume); + logger.trace("createCloudStackVolume : Creating Lun with cloudstackVolume request {} ", cloudstackVolume); if (cloudstackVolume == null || cloudstackVolume.getLun() == null) { logger.error("createCloudStackVolume: LUN creation failed. Invalid request: {}", cloudstackVolume); throw new CloudRuntimeException(" Failed to create Lun, invalid request"); @@ -78,7 +79,6 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume } Lun lun = createdLun.getRecords().get(0); logger.debug("createCloudStackVolume: LUN created successfully. Lun: {}", lun); - logger.info("createCloudStackVolume: LUN created successfully. LunName: {}", lun.getName()); CloudStackVolume createdCloudStackVolume = new CloudStackVolume(); createdCloudStackVolume.setLun(lun); @@ -104,7 +104,7 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { logger.error("deleteCloudStackVolume: Lun deletion failed. Invalid request: {}", cloudstackVolume); throw new CloudRuntimeException(" Failed to delete Lun, invalid request"); } - logger.info("deleteCloudStackVolume : Deleting Lun: {}", cloudstackVolume.getLun().getName()); + logger.trace("deleteCloudStackVolume : Deleting Lun: {}", cloudstackVolume.getLun().getName()); try { String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); Map queryParams = Map.of("allow_delete_while_mapped", "true"); @@ -117,7 +117,7 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { } throw feignEx; } - logger.info("deleteCloudStackVolume: Lun deleted successfully. LunName: {}", cloudstackVolume.getLun().getName()); + logger.trace("deleteCloudStackVolume: Lun deleted successfully. LunName: {}", cloudstackVolume.getLun().getName()); } catch (Exception e) { logger.error("Exception occurred while deleting Lun: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage()); throw new CloudRuntimeException("Failed to delete Lun: " + e.getMessage()); @@ -129,8 +129,7 @@ public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) {} @Override public CloudStackVolume getCloudStackVolume(Map values) { - logger.info("getCloudStackVolume : fetching Lun"); - logger.debug("getCloudStackVolume : fetching Lun with params {} ", values); + logger.trace("getCloudStackVolume : fetching Lun with params {} ", values); if (values == null || values.isEmpty()) { logger.error("getCloudStackVolume: get Lun failed. Invalid request: {}", values); throw new CloudRuntimeException(" get Lun Failed, invalid request"); @@ -151,7 +150,6 @@ public CloudStackVolume getCloudStackVolume(Map values) { } Lun lun = lunResponse.getRecords().get(0); logger.debug("getCloudStackVolume: Lun Details : {}", lun); - logger.info("getCloudStackVolume: Fetched the Lun successfully. LunName: {}", lun.getName()); CloudStackVolume cloudStackVolume = new CloudStackVolume(); cloudStackVolume.setLun(lun); @@ -171,23 +169,23 @@ public CloudStackVolume getCloudStackVolume(Map values) { @Override public AccessGroup createAccessGroup(AccessGroup accessGroup) { - logger.debug("createAccessGroup : Creating Igroup with access group request {} ", accessGroup); + logger.trace("createAccessGroup : Creating Igroup with access group request {} ", accessGroup); if (accessGroup == null) { logger.error("createAccessGroup: Igroup creation failed. Invalid request: {}", accessGroup); - throw new CloudRuntimeException(" Failed to create Igroup, invalid request"); + throw new CloudRuntimeException("Failed to create Igroup, invalid request"); } // Get StoragePool details if (accessGroup.getStoragePoolId() == null) { - throw new CloudRuntimeException(" Failed to create Igroup, invalid datastore details in the request"); + throw new CloudRuntimeException("Failed to create Igroup, invalid datastore details in the request"); } if (accessGroup.getHostsToConnect() == null || accessGroup.getHostsToConnect().isEmpty()) { - throw new CloudRuntimeException(" Failed to create Igroup, no hosts to connect provided in the request"); + throw new CloudRuntimeException("Failed to create Igroup, no hosts to connect provided in the request"); } String igroupName = null; try { Map dataStoreDetails = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId()); - logger.debug("createAccessGroup: Successfully fetched datastore details."); + logger.trace("createAccessGroup: Successfully fetched datastore details."); // Generate Igroup request Igroup igroupRequest = new Igroup(); @@ -196,7 +194,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { // Check if all hosts support the protocol if (!validateProtocolSupport(accessGroup.getHostsToConnect(), protocol)) { - String errMsg = " Not all hosts " + " support the protocol: " + protocol.name(); + String errMsg = "Not all hosts " + " support the protocol: " + protocol.name(); throw new CloudRuntimeException(errMsg); } @@ -218,7 +216,6 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { initiators.add(initiator); igroupRequest.setInitiators(initiators); igroupRequest.setDeleteOnUnmap(true); - igroupRequest.setDeleteOnUnmap(true); } igroupRequest.setProtocol(Igroup.ProtocolEnum.valueOf(OntapStorageConstants.ISCSI)); // Create Igroup @@ -239,7 +236,6 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { throw feignEx; } - logger.debug("createAccessGroup: createdIgroup: {}", createdIgroup); logger.debug("createAccessGroup: createdIgroup Records: {}", createdIgroup.getRecords()); if (createdIgroup.getRecords() == null || createdIgroup.getRecords().isEmpty()) { logger.error("createAccessGroup: Igroup creation failed for Igroup Name {}", igroupName); @@ -247,10 +243,9 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { } Igroup igroup = createdIgroup.getRecords().get(0); logger.debug("createAccessGroup: Successfully extracted igroup from response: {}", igroup); - logger.info("createAccessGroup: Igroup created successfully. IgroupName: {}", igroup.getName()); createdAccessGroup.setIgroup(igroup); - logger.debug("createAccessGroup: Returning createdAccessGroup"); + logger.trace("createAccessGroup: Returning createdAccessGroup"); return createdAccessGroup; } catch (Exception e) { logger.error("Exception occurred while creating Igroup: {}, Exception: {}", igroupName, e.getMessage(), e); @@ -260,61 +255,63 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { @Override public void deleteAccessGroup(AccessGroup accessGroup) { - logger.info("deleteAccessGroup: Deleting iGroup"); + logger.trace("deleteAccessGroup: Deleting iGroup"); if (accessGroup == null) { logger.error("deleteAccessGroup: Igroup deletion failed. Invalid request: {}", accessGroup); - throw new CloudRuntimeException(" Failed to delete Igroup, invalid request"); + throw new CloudRuntimeException("Failed to delete Igroup, invalid request"); } // Get StoragePool details if (accessGroup.getStoragePoolId() == null) { - throw new CloudRuntimeException(" Failed to delete Igroup, invalid datastore details in the request"); + throw new CloudRuntimeException("Failed to delete Igroup, invalid datastore details in the request"); } try { String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); String svmName = storage.getSvmName(); //Get iGroup name per host - for(HostVO host : accessGroup.getHostsToConnect()) { - String igroupName = OntapStorageUtils.getIgroupName(svmName, host.getName()); - logger.info("deleteAccessGroup: iGroup name '{}'", igroupName); - - // Get the iGroup to retrieve its UUID - Map igroupParams = Map.of( - OntapStorageConstants.SVM_DOT_NAME, svmName, - OntapStorageConstants.NAME, igroupName - ); - - try { - OntapResponse igroupResponse = sanFeignClient.getIgroupResponse(authHeader, igroupParams); - if (igroupResponse == null || igroupResponse.getRecords() == null || igroupResponse.getRecords().isEmpty()) { - logger.warn("deleteAccessGroup: iGroup '{}' not found, may have been already deleted", igroupName); - return; - } - - Igroup igroup = igroupResponse.getRecords().get(0); - String igroupUuid = igroup.getUuid(); - - if (igroupUuid == null || igroupUuid.isEmpty()) { - throw new CloudRuntimeException(" iGroup UUID is null or empty for iGroup: " + igroupName); - } + if(!CollectionUtils.isEmpty(accessGroup.getHostsToConnect())) { + for (HostVO host : accessGroup.getHostsToConnect()) { + String igroupName = OntapStorageUtils.getIgroupName(svmName, host.getName()); + logger.info("deleteAccessGroup: iGroup name '{}'", igroupName); - logger.info("deleteAccessGroup: Deleting iGroup '{}' with UUID '{}'", igroupName, igroupUuid); - - // Delete the iGroup using the UUID - sanFeignClient.deleteIgroup(authHeader, igroupUuid); - - logger.info("deleteAccessGroup: Successfully deleted iGroup '{}'", igroupName); - - } catch (FeignException e) { - if (e.status() == 404) { - logger.warn("deleteAccessGroup: iGroup '{}' does not exist (status 404), skipping deletion", igroupName); - } else { - logger.error("deleteAccessGroup: FeignException occurred: Status: {}, Exception: {}", e.status(), e.getMessage(), e); + // Get the iGroup to retrieve its UUID + Map igroupParams = Map.of( + OntapStorageConstants.SVM_DOT_NAME, svmName, + OntapStorageConstants.NAME, igroupName + ); + + try { + OntapResponse igroupResponse = sanFeignClient.getIgroupResponse(authHeader, igroupParams); + if (igroupResponse == null || igroupResponse.getRecords() == null || igroupResponse.getRecords().isEmpty()) { + logger.warn("deleteAccessGroup: iGroup '{}' not found, may have been already deleted", igroupName); + return; + } + + Igroup igroup = igroupResponse.getRecords().get(0); + String igroupUuid = igroup.getUuid(); + + if (igroupUuid == null || igroupUuid.isEmpty()) { + throw new CloudRuntimeException("iGroup UUID is null or empty for iGroup: " + igroupName); + } + + logger.info("deleteAccessGroup: Deleting iGroup '{}' with UUID '{}'", igroupName, igroupUuid); + + // Delete the iGroup using the UUID + sanFeignClient.deleteIgroup(authHeader, igroupUuid); + + logger.info("deleteAccessGroup: Successfully deleted iGroup '{}'", igroupName); + + } catch (FeignException e) { + if (e.status() == 404) { + logger.warn("deleteAccessGroup: iGroup '{}' does not exist (status 404), skipping deletion", igroupName); + } else { + logger.error("deleteAccessGroup: FeignException occurred: Status: {}, Exception: {}", e.status(), e.getMessage(), e); + throw e; + } + } catch (Exception e) { + logger.error("deleteAccessGroup: Exception occurred: {}", e.getMessage(), e); throw e; } - } catch (Exception e) { - logger.error("deleteAccessGroup: Exception occurred: {}", e.getMessage(), e); - throw e; } } } catch (FeignException e) { @@ -333,7 +330,7 @@ private boolean validateProtocolSupport(List hosts, ProtocolType protoco return false; } } - logger.info("validateProtocolSupportAndFetchHostsIdentifier: All hosts support the protocol: " + protocolType.name()); + logger.trace("validateProtocolSupportAndFetchHostsIdentifier: All hosts support the protocol: " + protocolType.name()); return true; } @@ -344,17 +341,16 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) { @Override public AccessGroup getAccessGroup(Map values) { - logger.info("getAccessGroup : fetch Igroup"); - logger.debug("getAccessGroup : fetching Igroup with params {} ", values); + logger.trace("getAccessGroup : fetching Igroup with params {} ", values); if (values == null || values.isEmpty()) { logger.error("getAccessGroup: get Igroup failed. Invalid request: {}", values); - throw new CloudRuntimeException(" get Igroup Failed, invalid request"); + throw new CloudRuntimeException("get Igroup Failed, invalid request"); } String svmName = values.get(OntapStorageConstants.SVM_DOT_NAME); String igroupName = values.get(OntapStorageConstants.NAME); if (svmName == null || igroupName == null || svmName.isEmpty() || igroupName.isEmpty()) { logger.error("getAccessGroup: get Igroup failed. Invalid svm:{} or igroup name: {}", svmName, igroupName); - throw new CloudRuntimeException(" Failed to get Igroup, invalid request"); + throw new CloudRuntimeException("Failed to get Igroup, invalid request"); } try { String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword()); @@ -381,20 +377,20 @@ public AccessGroup getAccessGroup(Map values) { } } - public Map enableLogicalAccess(Map values) { - logger.info("enableLogicalAccess : Create LunMap"); - logger.debug("enableLogicalAccess : Creating LunMap with values {} ", values); - Map response = null; + @Override + public String enableLogicalAccess(Map values) { + logger.trace("enableLogicalAccess : Creating LunMap with values {} ", values); + String lunNumber = null; if (values == null) { logger.error("enableLogicalAccess: LunMap creation failed. Invalid request values: null"); - throw new CloudRuntimeException(" Failed to create LunMap, invalid request"); + throw new CloudRuntimeException("Failed to create LunMap, invalid request"); } String svmName = values.get(OntapStorageConstants.SVM_DOT_NAME); String lunName = values.get(OntapStorageConstants.LUN_DOT_NAME); String igroupName = values.get(OntapStorageConstants.IGROUP_DOT_NAME); if (svmName == null || lunName == null || igroupName == null || svmName.isEmpty() || lunName.isEmpty() || igroupName.isEmpty()) { logger.error("enableLogicalAccess: LunMap creation failed. Invalid request values: {}", values); - throw new CloudRuntimeException(" Failed to create LunMap, invalid request"); + throw new CloudRuntimeException("Failed to create LunMap, invalid request"); } try { // Get AuthHeader @@ -433,25 +429,28 @@ public Map enableLogicalAccess(Map values) { OntapStorageConstants.IGROUP_DOT_NAME, igroupName, OntapStorageConstants.FIELDS, OntapStorageConstants.LOGICAL_UNIT_NUMBER )); - response = Map.of( - OntapStorageConstants.LOGICAL_UNIT_NUMBER, lunMapResponse.getRecords().get(0).getLogicalUnitNumber().toString() - ); + if(lunMapResponse != null && lunMapResponse.getRecords() != null && !lunMapResponse.getRecords().isEmpty() + && lunMapResponse.getRecords().get(0).getLogicalUnitNumber() != null) { + lunNumber = lunMapResponse.getRecords().get(0).getLogicalUnitNumber().toString(); + + } else { + logger.error("enableLogicalAccess: Failed to fetch LunMap details for Lun: {} and igroup: {}. LunMap response is null or empty.", lunName, igroupName); + throw new CloudRuntimeException("Failed to fetch LunMap details for Lun: " + lunName + " and igroup: " + igroupName); + } } catch (Exception e) { 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); } - logger.debug("enableLogicalAccess: LunMap created successfully, LunMap: {}", lunMapResponse.getRecords().get(0)); - logger.info("enableLogicalAccess: LunMap created successfully."); + logger.trace("enableLogicalAccess: LunMap created successfully, LunMap: {}", lunMapResponse.getRecords().get(0)); } catch (Exception e) { logger.error("Exception occurred while creating LunMap", e); throw new CloudRuntimeException("Failed to create LunMap: " + e.getMessage()); } - return response; + return lunNumber; } public void disableLogicalAccess(Map values) { - logger.info("disableLogicalAccess : Delete LunMap"); - logger.debug("disableLogicalAccess : Deleting LunMap with values {} ", values); + logger.trace("disableLogicalAccess : Deleting LunMap with values {} ", values); if (values == null) { logger.error("disableLogicalAccess: LunMap deletion failed. Invalid request values: null"); throw new CloudRuntimeException(" Failed to delete LunMap, invalid request"); @@ -480,9 +479,10 @@ public void disableLogicalAccess(Map values) { } // GET-only helper: fetch LUN-map and return logical unit number if it exists; otherwise return null - public Map getLogicalAccess(Map values) { - logger.info("getLogicalAccess : Fetch LunMap"); - logger.debug("getLogicalAccess : Fetching LunMap with values {} ", values); + @Override + public String getLogicalAccess(Map values) { + String lunNumber = null; + logger.trace("getLogicalAccess : Fetching LunMap with values {} ", values); if (values == null) { logger.error("getLogicalAccess: Invalid request values: null"); throw new CloudRuntimeException(" Invalid request"); @@ -504,19 +504,18 @@ public Map getLogicalAccess(Map values) { OntapStorageConstants.FIELDS, OntapStorageConstants.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(OntapStorageConstants.LOGICAL_UNIT_NUMBER, lunNumber) : null; + Integer lunLogicalUnitNum = lunMapResponse.getRecords().get(0).getLogicalUnitNumber(); + lunNumber = lunLogicalUnitNum != null ? lunLogicalUnitNum.toString() : null; } } catch (Exception e) { logger.warn("getLogicalAccess: LunMap not found for Lun: {} and igroup: {} ({}).", lunName, igroupName, e.getMessage()); } - return null; + return lunNumber; } @Override public String ensureLunMapped(String svmName, String lunName, String accessGroupName) { - logger.info("ensureLunMapped: Ensuring LUN [{}] is mapped to igroup [{}] on SVM [{}]", lunName, accessGroupName, svmName); + logger.trace("ensureLunMapped: Ensuring LUN [{}] is mapped to igroup [{}] on SVM [{}]", lunName, accessGroupName, svmName); // Check existing map first Map getMap = Map.of( @@ -524,9 +523,8 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup OntapStorageConstants.SVM_DOT_NAME, svmName, OntapStorageConstants.IGROUP_DOT_NAME, accessGroupName ); - Map mapResp = getLogicalAccess(getMap); - if (mapResp != null && mapResp.containsKey(OntapStorageConstants.LOGICAL_UNIT_NUMBER)) { - String lunNumber = mapResp.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER); + String lunNumber = getLogicalAccess(getMap); + if (lunNumber != null) { logger.info("ensureLunMapped: Existing LunMap found for LUN [{}] in igroup [{}] with LUN number [{}]", lunName, accessGroupName, lunNumber); return lunNumber; } @@ -537,12 +535,12 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup OntapStorageConstants.SVM_DOT_NAME, svmName, OntapStorageConstants.IGROUP_DOT_NAME, accessGroupName ); - Map response = enableLogicalAccess(enableMap); - if (response == null || !response.containsKey(OntapStorageConstants.LOGICAL_UNIT_NUMBER)) { + String response = enableLogicalAccess(enableMap); + if (response == null ) { throw new CloudRuntimeException("Failed to map LUN [" + lunName + "] to iGroup [" + accessGroupName + "]"); } - logger.info("ensureLunMapped: Successfully mapped LUN [{}] to igroup [{}] with LUN number [{}]", lunName, accessGroupName, response.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER)); - return response.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER); + logger.trace("ensureLunMapped: Successfully mapped LUN [{}] to igroup [{}] with LUN number [{}]", lunName, accessGroupName, response); + return response; } /** * Reverts a LUN to a snapshot using the ONTAP CLI-based snapshot file restore API. @@ -565,7 +563,7 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, String lunUuid, String flexVolName) { - logger.info("revertSnapshotForCloudStackVolume [iSCSI]: Restoring LUN [{}] from snapshot [{}] on FlexVol [{}]", + logger.trace("revertSnapshotForCloudStackVolume [iSCSI]: Restoring LUN [{}] from snapshot [{}] on FlexVol [{}]", volumePath, snapshotName, flexVolName); if (snapshotName == null || snapshotName.isEmpty()) { @@ -588,7 +586,7 @@ public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( svmName, flexVolName, snapshotName, ontapLunPath); - logger.info("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", + logger.trace("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", svmName, flexVolName, snapshotName, ontapLunPath); return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); 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 975a74df85aa..9815724fc1aa 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 @@ -54,6 +54,7 @@ public void setPolicy(ExportPolicy policy) { public List getHostsToConnect() { return hostsToConnect; } + public void setHostsToConnect(List hostsToConnect) { this.hostsToConnect = hostsToConnect; } @@ -69,6 +70,7 @@ public void setStoragePoolId(Long storagePoolId) { public Scope getScope() { return scope; } + public void setScope(Scope scope) { this.scope = scope; } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java index 3edf02000cf2..ab38e3045f51 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/model/CloudStackVolume.java @@ -36,18 +36,23 @@ public class CloudStackVolume { * a. snapshot workflows will get source LUN details from it. */ private Lun lun; + private String datastoreId; + /** * FlexVolume UUID on which this cloudstack volume is created. * a. Field is eligible for unified storage only. * b. It will be null for the disaggregated storage. */ private String flexVolumeUuid; + /** * Field serves for snapshot workflows */ private String destinationPath; + private DataObject volumeInfo; // This is needed as we need DataObject to be passed to agent to create volume + public FileInfo getFile() { return file; } @@ -63,26 +68,37 @@ public Lun getLun() { public void setLun(Lun lun) { this.lun = lun; } + public String getDatastoreId() { return datastoreId; } + public void setDatastoreId(String datastoreId) { this.datastoreId = datastoreId; } + public DataObject getVolumeInfo() { return volumeInfo; } + public void setVolumeInfo(DataObject volumeInfo) { this.volumeInfo = volumeInfo; } + public String getFlexVolumeUuid() { return flexVolumeUuid; } + public void setFlexVolumeUuid(String flexVolumeUuid) { this.flexVolumeUuid = flexVolumeUuid; } - public String getDestinationPath() { return this.destinationPath; } - public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; } + public String getDestinationPath() { + return this.destinationPath; + } + + public void setDestinationPath(String destinationPath) { + this.destinationPath = destinationPath; + } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java index 2d6e4a4530ea..d0ea1783aa1d 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageConstants.java @@ -19,15 +19,20 @@ package org.apache.cloudstack.storage.utils; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; public class OntapStorageConstants { - public static final String ONTAP_PLUGIN_NAME = "NetApp ONTAP"; + /** Same as {@link DataStoreProvider#ONTAP_PLUGIN_NAME}; retained for existing ONTAP plugin call sites. */ + public static final String ONTAP_PLUGIN_NAME = DataStoreProvider.ONTAP_PLUGIN_NAME; public static final int NFS3_PORT = 2049; public static final int ISCSI_PORT = 3260; public static final String NFS = "nfs"; public static final String ISCSI = "iscsi"; + + public static final String NFS_ENABLED = "nfs.enabled"; + public static final String ISCSI_ENABLED = "iscsi.enabled"; public static final String SIZE = "size"; public static final String PROTOCOL = "protocol"; public static final String SVM_NAME = "svmName"; @@ -42,7 +47,6 @@ public class OntapStorageConstants { 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/OntapStorageUtils.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageUtils.java index 22c30c1256ac..596372edcf16 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageUtils.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/OntapStorageUtils.java @@ -35,6 +35,8 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.util.Base64Utils; + +import java.nio.charset.StandardCharsets; import java.util.Map; public class OntapStorageUtils { @@ -51,7 +53,7 @@ public class OntapStorageUtils { * @return */ public static String generateAuthHeader (String username, String password) { - byte[] encodedBytes = Base64Utils.encode((username + AUTH_HEADER_COLON + password).getBytes()); + byte[] encodedBytes = Base64Utils.encode((username + AUTH_HEADER_COLON + password).getBytes(StandardCharsets.UTF_8)); return BASIC + StringUtils.SPACE + new String(encodedBytes); } @@ -106,7 +108,7 @@ public static boolean isValidName(String name) { return name.matches(OntapStorageConstants.ONTAP_NAME_REGEX); } - public static String getOSTypeFromHypervisor(String hypervisorType){ + public static String getOSTypeFromHypervisor(String hypervisorType) { switch (hypervisorType) { case OntapStorageConstants.KVM: return Lun.OsTypeEnum.LINUX.name(); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java index 3bb32ae8ba1c..a71df4c2e349 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java @@ -27,6 +27,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.StrategyPriority; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; @@ -233,7 +234,7 @@ boolean allVolumesOnOntapManagedStorage(long vmId) { volume.getId(), pool.getName()); return false; } - if (!OntapStorageConstants.ONTAP_PLUGIN_NAME.equals(pool.getStorageProviderName())) { + if (!DataStoreProvider.ONTAP_PLUGIN_NAME.equals(pool.getStorageProviderName())) { logger.debug("allVolumesOnOntapManagedStorage: Volume [{}] is on managed pool [{}] with provider [{}], not ONTAP", volume.getId(), pool.getName(), pool.getStorageProviderName()); return false; @@ -293,8 +294,8 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { GuestOSVO guestOS = guestOSDao.findById(userVm.getGuestOSId()); List volumeTOs = vmSnapshotHelper.getVolumeTOList(userVm.getId()); - long prev_chain_size = 0; - long virtual_size = 0; + long prevChainSize = 0; + long virtualSize = 0; // Build snapshot parent chain VMSnapshotTO current = null; @@ -304,20 +305,20 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } // Respect the user's quiesce option from the VM snapshot request - boolean quiescevm = true; // default to true for safety + boolean quiesceVm = true; // default to true for safety VMSnapshotOptions options = vmSnapshotVO.getOptions(); if (options != null) { - quiescevm = options.needQuiesceVM(); + quiesceVm = options.needQuiesceVM(); } // Check if VM is actually running - freeze/thaw only makes sense for running VMs boolean vmIsRunning = VirtualMachine.State.Running.equals(userVm.getState()); - boolean shouldFreezeThaw = quiescevm && vmIsRunning; + boolean shouldFreezeThaw = quiesceVm && vmIsRunning; if (!vmIsRunning) { logger.info("takeVMSnapshot: VM [{}] is in state [{}] (not Running). Skipping freeze/thaw - " + "FlexVolume snapshot will be taken directly.", userVm.getInstanceName(), userVm.getState()); - } else if (quiescevm) { + } else if (quiesceVm) { logger.info("takeVMSnapshot: Quiesce option is enabled for ONTAP VM Snapshot of VM [{}]. " + "VM file systems will be frozen/thawed for application-consistent snapshots.", userVm.getInstanceName()); } else { @@ -326,7 +327,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } VMSnapshotTO target = new VMSnapshotTO(vmSnapshot.getId(), vmSnapshot.getName(), - vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, current, quiescevm); + vmSnapshot.getType(), null, vmSnapshot.getDescription(), false, current, quiesceVm); if (current == null) { vmSnapshotVO.setParent(null); @@ -337,13 +338,13 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { CreateVMSnapshotCommand ccmd = new CreateVMSnapshotCommand( userVm.getInstanceName(), userVm.getUuid(), target, volumeTOs, guestOS.getDisplayName()); - logger.info("takeVMSnapshot: Creating ONTAP FlexVolume VM Snapshot for VM [{}] with quiesce={}", userVm.getInstanceName(), quiescevm); + logger.info("takeVMSnapshot: Creating ONTAP FlexVolume VM Snapshot for VM [{}] with quiesce={}", userVm.getInstanceName(), quiesceVm); // Prepare volume info list and calculate sizes for (VolumeObjectTO volumeObjectTO : volumeTOs) { - virtual_size += volumeObjectTO.getSize(); + virtualSize += volumeObjectTO.getSize(); VolumeVO volumeVO = volumeDao.findById(volumeObjectTO.getId()); - prev_chain_size += volumeVO.getVmSnapshotChainSize() == null ? 0 : volumeVO.getVmSnapshotChainSize(); + prevChainSize += volumeVO.getVmSnapshotChainSize() == null ? 0 : volumeVO.getVmSnapshotChainSize(); } // ── Group volumes by FlexVolume UUID ── @@ -371,7 +372,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { logger.info("takeVMSnapshot: VM [{}] frozen successfully via QEMU guest agent", userVm.getInstanceName()); } else { logger.info("takeVMSnapshot: Skipping VM freeze for VM [{}] (quiesce={}, vmIsRunning={})", - userVm.getInstanceName(), quiescevm, vmIsRunning); + userVm.getInstanceName(), quiesceVm, vmIsRunning); } // ── Step 2: Create FlexVolume-level snapshots ── @@ -402,7 +403,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } // Poll for job completion - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2); + Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); if (!jobSucceeded) { throw new CloudRuntimeException("FlexVolume snapshot job failed on FlexVol UUID [" + flexVolUuid + "]"); } @@ -427,7 +428,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } } finally { // ── Step 3: Thaw the VM (only if it was frozen, always even on error) ── - if (quiescevm && freezeAnswer != null && freezeAnswer.getResult()) { + if (quiesceVm && freezeAnswer != null && freezeAnswer.getResult()) { try { thawAnswer = (FreezeThawVMAnswer) agentMgr.send(hostId, thawCmd); if (thawAnswer != null && thawAnswer.getResult()) { @@ -458,13 +459,13 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { logger.info("takeVMSnapshot: ONTAP FlexVolume VM Snapshot [{}] created successfully for VM [{}] ({} FlexVol snapshot(s))", vmSnapshot.getName(), userVm.getInstanceName(), createdSnapshots.size()); - long new_chain_size = 0; + long newChainSize = 0; for (VolumeObjectTO volumeTo : answer.getVolumeTOs()) { publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_CREATE, vmSnapshot, userVm, volumeTo); - new_chain_size += volumeTo.getSize(); + newChainSize += volumeTo.getSize(); } publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_ON_PRIMARY, vmSnapshot, userVm, - new_chain_size - prev_chain_size, virtual_size); + newChainSize - prevChainSize, virtualSize); result = true; return vmSnapshot; @@ -475,8 +476,6 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } catch (AgentUnavailableException e) { logger.error("takeVMSnapshot: ONTAP VM Snapshot [{}] failed, agent unavailable: {}", vmSnapshot.getName(), e.getMessage()); throw new CloudRuntimeException("Creating Instance Snapshot: " + vmSnapshot.getName() + " failed: " + e.getMessage()); - } catch (CloudRuntimeException e) { - throw e; } finally { if (!result) { // Rollback all FlexVolume snapshots created so far (deduplicate by FlexVol+Snapshot) @@ -559,12 +558,12 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { } processAnswer(vmSnapshotVO, userVm, new DeleteVMSnapshotAnswer(deleteSnapshotCommand, volumeTOs), null); - long full_chain_size = 0; + long fullChainSize = 0; for (VolumeObjectTO volumeTo : volumeTOs) { publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_DELETE, vmSnapshot, userVm, volumeTo); - full_chain_size += volumeTo.getSize(); + fullChainSize += volumeTo.getSize(); } - publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshot, userVm, full_chain_size, 0L); + publishUsageEvent(EventTypes.EVENT_VM_SNAPSHOT_OFF_PRIMARY, vmSnapshot, userVm, fullChainSize, 0L); return true; } catch (CloudRuntimeException err) { String errMsg = String.format("Delete of ONTAP VM Snapshot [%s] of VM [%s] failed: %s", @@ -745,7 +744,7 @@ void rollbackFlexVolSnapshot(FlexVolSnapshotDetail detail) { JobResponse jobResponse = client.deleteSnapshot(authHeader, detail.flexVolUuid, detail.snapshotUuid); if (jobResponse != null && jobResponse.getJob() != null) { - storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 10, 2); + storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 10, 2000); } } catch (Exception e) { logger.error("rollbackFlexVolSnapshot: Rollback of FlexVol snapshot failed: {}", e.getMessage(), e); @@ -779,7 +778,7 @@ void deleteFlexVolSnapshots(List flexVolDetails) { JobResponse jobResponse = client.deleteSnapshot(authHeader, detail.flexVolUuid, detail.snapshotUuid); if (jobResponse != null && jobResponse.getJob() != null) { - storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2); + storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); } deletedSnapshots.put(dedupeKey, Boolean.TRUE); @@ -847,7 +846,7 @@ void revertFlexVolSnapshots(List flexVolDetails) { JobResponse jobResponse = snapshotClient.restoreFileFromSnapshotCli(authHeader, restoreRequest); if (jobResponse != null && jobResponse.getJob() != null) { - Boolean success = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2); + Boolean success = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2000); if (!success) { throw new CloudRuntimeException("Snapshot file restore failed for volume path [" + ontapFilePath + "] from snapshot [" + detail.snapshotName + diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java index 68fd40d5b7f1..b535217fd235 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriverTest.java @@ -158,7 +158,6 @@ void testCreateAsync_NullCallback_ThrowsException() { void testCreateAsync_VolumeWithISCSI_Success() { // Setup when(dataStore.getId()).thenReturn(1L); - when(dataStore.getUuid()).thenReturn("pool-uuid-123"); when(dataStore.getName()).thenReturn("ontap-pool"); when(volumeInfo.getType()).thenReturn(VOLUME); when(volumeInfo.getId()).thenReturn(100L); @@ -212,7 +211,6 @@ void testCreateAsync_VolumeWithNFS_Success() { storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name()); when(dataStore.getId()).thenReturn(1L); - when(dataStore.getUuid()).thenReturn("pool-uuid-123"); when(dataStore.getName()).thenReturn("ontap-pool"); when(volumeInfo.getType()).thenReturn(VOLUME); when(volumeInfo.getId()).thenReturn(100L); @@ -336,7 +334,6 @@ void testGrantAccess_NullParameters_ThrowsException() { void testGrantAccess_ClusterScope_Success() { // Setup when(dataStore.getId()).thenReturn(1L); - when(dataStore.getUuid()).thenReturn("pool-uuid-123"); when(volumeInfo.getType()).thenReturn(VOLUME); when(volumeInfo.getId()).thenReturn(100L); @@ -389,7 +386,6 @@ void testGrantAccess_IgroupNotFound_CreatesNewIgroup() { when(hostVO.getName()).thenReturn("host1"); when(dataStore.getId()).thenReturn(1L); - when(dataStore.getUuid()).thenReturn("pool-uuid-123"); when(volumeInfo.getType()).thenReturn(VOLUME); when(volumeInfo.getId()).thenReturn(100L); diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProviderTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProviderTest.java index 0d8ac53a1f98..d3fa21e4189c 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProviderTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/provider/OntapPrimaryDatastoreProviderTest.java @@ -19,6 +19,7 @@ package org.apache.cloudstack.storage.provider; import com.cloud.utils.component.ComponentContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreDriver; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreLifeCycle; import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider.DataStoreProviderType; @@ -26,7 +27,6 @@ import org.apache.cloudstack.storage.driver.OntapPrimaryDatastoreDriver; import org.apache.cloudstack.storage.lifecycle.OntapPrimaryDatastoreLifecycle; import org.apache.cloudstack.storage.listener.OntapHostListener; -import org.apache.cloudstack.storage.utils.OntapStorageConstants; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -60,7 +60,7 @@ void setUp() { @Test public void testGetName() { String name = provider.getName(); - assertEquals(OntapStorageConstants.ONTAP_PLUGIN_NAME, name); + assertEquals(DataStoreProvider.ONTAP_PLUGIN_NAME, name); } @Test diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java index c2a4b56a1fa1..86ef1d7c79b6 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java @@ -53,7 +53,6 @@ import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -121,29 +120,26 @@ private void injectMockedClient(String fieldName, Object mockedClient) { } @Override - public org.apache.cloudstack.storage.service.model.CloudStackVolume createCloudStackVolume( - org.apache.cloudstack.storage.service.model.CloudStackVolume cloudstackVolume) { + public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume) { return null; } @Override - org.apache.cloudstack.storage.service.model.CloudStackVolume updateCloudStackVolume( - org.apache.cloudstack.storage.service.model.CloudStackVolume cloudstackVolume) { + CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) { return null; } @Override - public void deleteCloudStackVolume(org.apache.cloudstack.storage.service.model.CloudStackVolume cloudstackVolume) { + public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - public void copyCloudStackVolume(org.apache.cloudstack.storage.service.model.CloudStackVolume cloudstackVolume) { + public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - public CloudStackVolume getCloudStackVolume( - Map cloudStackVolumeMap) { + public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap) { return null; } @@ -153,29 +149,26 @@ public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String } @Override - public AccessGroup createAccessGroup( - org.apache.cloudstack.storage.service.model.AccessGroup accessGroup) { + public AccessGroup createAccessGroup(AccessGroup accessGroup) { return null; } @Override - public void deleteAccessGroup(org.apache.cloudstack.storage.service.model.AccessGroup accessGroup) { + public void deleteAccessGroup(AccessGroup accessGroup) { } @Override - AccessGroup updateAccessGroup( - org.apache.cloudstack.storage.service.model.AccessGroup accessGroup) { + AccessGroup updateAccessGroup(AccessGroup accessGroup) { return null; } @Override - public AccessGroup getAccessGroup( - Map values) { + public AccessGroup getAccessGroup(Map values) { return null; } @Override - public Map enableLogicalAccess(Map values) { + public String enableLogicalAccess(Map values) { return null; } @@ -184,7 +177,7 @@ public void disableLogicalAccess(Map values) { } @Override - public Map getLogicalAccess(Map values) { + public String getLogicalAccess(Map values) { return null; } } @@ -245,11 +238,9 @@ public void testConnect_svmNotFound() { when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse); - // Execute - boolean result = storageStrategy.connect(); - - // Verify - assertFalse(result, "connect() should return false when SVM is not found"); + // Execute & Verify + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("No SVM found")); } @Test @@ -265,11 +256,9 @@ public void testConnect_svmNotRunning() { when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse); - // Execute - boolean result = storageStrategy.connect(); - - // Verify - assertFalse(result, "connect() should return false when SVM is not running"); + // Execute & Verify + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("not in running state")); } @Test @@ -291,8 +280,8 @@ public void testConnect_nfsNotEnabled() { when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse); // Execute & Verify - boolean result = storageStrategy.connect(); - assertFalse(result, "connect() should fail when NFS is disabled"); + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("NFS protocol is not enabled")); } @Test @@ -320,8 +309,8 @@ public void testConnect_iscsiNotEnabled() { when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse); // Execute & Verify - boolean result = storageStrategy.connect(); - assertFalse(result, "connect() should fail when iSCSI is disabled"); + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("ISCSI protocol is not enabled")); } @Test @@ -338,11 +327,9 @@ public void testConnect_noAggregates() { when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(svmResponse); - // Execute - boolean result = storageStrategy.connect(); - - // Verify - assertFalse(result, "connect() should return false when no aggregates are assigned"); + // Execute & Verify + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("No aggregates")); } @Test @@ -350,11 +337,9 @@ public void testConnect_nullSvmResponse() { // Setup when(svmFeignClient.getSvmResponse(anyMap(), anyString())).thenReturn(null); - // Execute - boolean result = storageStrategy.connect(); - - // Verify - assertFalse(result, "connect() should return false when SVM response is null"); + // Execute & Verify + CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.connect()); + assertTrue(ex.getMessage().contains("No SVM found")); } // ========== createStorageVolume() Tests ========== diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java index b3f2364656a7..1c0c84ef91dd 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedSANStrategyTest.java @@ -516,13 +516,11 @@ void testEnableLogicalAccess_Success() { when(sanFeignClient.getLunMapResponse(eq(authHeader), anyMap())).thenReturn(response); // Execute - Map result = unifiedSANStrategy.enableLogicalAccess(values); + String result = unifiedSANStrategy.enableLogicalAccess(values); // Verify assertNotNull(result); - assertTrue(result.containsKey(OntapStorageConstants.LOGICAL_UNIT_NUMBER)); - assertEquals("0", result.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER)); - + assertEquals("0", result); verify(sanFeignClient).createLunMap(eq(authHeader), eq(true), any(LunMap.class)); } } @@ -550,11 +548,11 @@ void testEnableLogicalAccess_AlreadyMapped_ReturnsLunNumber() { when(sanFeignClient.getLunMapResponse(eq(authHeader), anyMap())).thenReturn(response); // Execute - Map result = unifiedSANStrategy.enableLogicalAccess(values); + String result = unifiedSANStrategy.enableLogicalAccess(values); // Verify assertNotNull(result); - assertEquals("5", result.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER)); + assertEquals("5", result); } } @@ -621,11 +619,11 @@ void testGetLogicalAccess_Success() { when(sanFeignClient.getLunMapResponse(eq(authHeader), anyMap())).thenReturn(response); // Execute - Map result = unifiedSANStrategy.getLogicalAccess(values); + String result = unifiedSANStrategy.getLogicalAccess(values); // Verify assertNotNull(result); - assertEquals("3", result.get(OntapStorageConstants.LOGICAL_UNIT_NUMBER)); + assertEquals("3", result); } } @@ -645,7 +643,7 @@ void testGetLogicalAccess_NotFound_ReturnsNull() { .thenThrow(new RuntimeException("Not found")); // Execute - Map result = unifiedSANStrategy.getLogicalAccess(values); + String result = unifiedSANStrategy.getLogicalAccess(values); // Verify assertNull(result); @@ -1671,7 +1669,7 @@ void testGetLogicalAccess_EmptyResponse_ReturnsNull() { when(sanFeignClient.getLunMapResponse(eq(authHeader), anyMap())).thenReturn(emptyResponse); - Map result = unifiedSANStrategy.getLogicalAccess(values); + String result = unifiedSANStrategy.getLogicalAccess(values); assertNull(result); } @@ -1691,7 +1689,7 @@ void testGetLogicalAccess_ExceptionThrown_ReturnsNull() { when(sanFeignClient.getLunMapResponse(eq(authHeader), anyMap())) .thenThrow(new RuntimeException("Connection failed")); - Map result = unifiedSANStrategy.getLogicalAccess(values); + String result = unifiedSANStrategy.getLogicalAccess(values); assertNull(result); } diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java index 2fa9e77a20cd..b069ab7246a0 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -46,9 +45,9 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; import org.apache.cloudstack.storage.to.VolumeObjectTO; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; @@ -106,9 +105,6 @@ class OntapVMSnapshotStrategyTest { private static final String VM_INSTANCE_NAME = "i-2-100-VM"; private static final String VM_UUID = "vm-uuid-123"; - @Spy - private OntapVMSnapshotStrategy strategy; - @Mock private UserVmDao userVmDao; @Mock @@ -132,36 +128,9 @@ class OntapVMSnapshotStrategyTest { @Mock private VolumeDetailsDao volumeDetailsDao; - @BeforeEach - void setUp() throws Exception { - // Inject mocks into the inherited fields via reflection - // DefaultVMSnapshotStrategy fields - setField(strategy, DefaultVMSnapshotStrategy.class, "vmSnapshotHelper", vmSnapshotHelper); - setField(strategy, DefaultVMSnapshotStrategy.class, "guestOSDao", guestOSDao); - setField(strategy, DefaultVMSnapshotStrategy.class, "userVmDao", userVmDao); - setField(strategy, DefaultVMSnapshotStrategy.class, "vmSnapshotDao", vmSnapshotDao); - setField(strategy, DefaultVMSnapshotStrategy.class, "agentMgr", agentMgr); - setField(strategy, DefaultVMSnapshotStrategy.class, "volumeDao", volumeDao); - - // StorageVMSnapshotStrategy fields - setField(strategy, StorageVMSnapshotStrategy.class, "storagePool", storagePool); - setField(strategy, StorageVMSnapshotStrategy.class, "vmSnapshotDetailsDao", vmSnapshotDetailsDao); - setField(strategy, StorageVMSnapshotStrategy.class, "volumeDataFactory", volumeDataFactory); - - // OntapVMSnapshotStrategy fields - setField(strategy, OntapVMSnapshotStrategy.class, "storagePoolDetailsDao", storagePoolDetailsDao); - setField(strategy, OntapVMSnapshotStrategy.class, "volumeDetailsDao", volumeDetailsDao); - } - - // ────────────────────────────────────────────────────────────────────────── - // Helper: inject field via reflection into a specific declaring class - // ────────────────────────────────────────────────────────────────────────── - - private void setField(Object target, Class declaringClass, String fieldName, Object value) throws Exception { - Field field = declaringClass.getDeclaredField(fieldName); - field.setAccessible(true); - field.set(target, value); - } + @Spy + @InjectMocks + private OntapVMSnapshotStrategy strategy; // ────────────────────────────────────────────────────────────────────────── // Helper: create common mocks diff --git a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java index a7c700debb74..aa90caa44295 100644 --- a/server/src/main/java/com/cloud/storage/StorageManagerImpl.java +++ b/server/src/main/java/com/cloud/storage/StorageManagerImpl.java @@ -3105,7 +3105,7 @@ public void updateStoragePoolHostVOAndBytes(StoragePool pool, long hostId, Modif } StoragePoolVO poolVO = _storagePoolDao.findById(pool.getId()); - if (!Storage.StoragePoolType.StorPool.equals(poolVO.getPoolType())) { + if (!Storage.StoragePoolType.StorPool.equals(poolVO.getPoolType()) && !DataStoreProvider.ONTAP_PLUGIN_NAME.equals(poolVO.getStorageProviderName())) { poolVO.setUsedBytes(mspAnswer.getPoolInfo().getCapacityBytes() - mspAnswer.getPoolInfo().getAvailableBytes()); poolVO.setCapacityBytes(mspAnswer.getPoolInfo().getCapacityBytes()); } diff --git a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java index fb5becfe9d25..f57604b4e973 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -33,6 +33,7 @@ import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.command.user.vmsnapshot.ListVMSnapshotCmd; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreProvider; import org.apache.cloudstack.engine.subsystem.api.storage.StorageStrategyFactory; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotStrategy; @@ -392,7 +393,7 @@ public VMSnapshot allocVMSnapshot(Long vmId, String vsDisplayName, String vsDesc if (snapshotStrategy == null) { // Check if this is ONTAP managed storage with memory snapshot request - provide specific error message if (snapshotMemory && rootVolumePool.isManaged() && - "ONTAP".equals(rootVolumePool.getStorageProviderName())) { + DataStoreProvider.ONTAP_PLUGIN_NAME.equals(rootVolumePool.getStorageProviderName())) { String message = String.format("Memory snapshots (snapshotmemory=true) are not supported for VMs on ONTAP managed storage. " + "Instance [%s] uses ONTAP storage which only supports disk-only (crash-consistent) snapshots. " + "Please use snapshotmemory=false for disk-only snapshots.", userVmVo.getUuid()); diff --git a/ui/src/views/infra/zone/StaticInputsForm.vue b/ui/src/views/infra/zone/StaticInputsForm.vue index 05493555acae..c5a296a42b37 100644 --- a/ui/src/views/infra/zone/StaticInputsForm.vue +++ b/ui/src/views/infra/zone/StaticInputsForm.vue @@ -247,15 +247,6 @@ export default { return Promise.resolve() } }, - async checkNumberFormat (rule, value) { - if (!value || value === '') { - return Promise.resolve() - } else if (!/^\d+$/.test(String(value).replace(/,/g, ''))) { - return Promise.reject(rule.message) - } else { - return Promise.resolve() - } - }, isDisplayInput (field) { if (!field.display && !field.hidden) { return true