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/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..116fff785596 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 @@ -328,12 +328,12 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe 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())); + 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..7b22c123cc78 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/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 +418,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..b7c962300506 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; @@ -109,7 +109,7 @@ public DataTO getTO(DataObject data) { public DataStoreTO getStoreTO(DataStore store) { return null; } @Override - public boolean volumesRequireGrantAccessWhenUsed(){ + public boolean volumesRequireGrantAccessWhenUsed() { logger.info("OntapPrimaryDatastoreDriver: volumesRequireGrantAccessWhenUsed: Called"); return true; } @@ -141,7 +141,6 @@ 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()); @@ -149,7 +148,7 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet VolumeInfo volInfo = (VolumeInfo) dataObject; // Create the backend storage object (LUN for iSCSI, no-op for NFS) - CloudStackVolume created = createCloudStackVolume(dataStore, volInfo, details); + CloudStackVolume created = createCloudStackVolume(storagePool, volInfo, details); // Update CloudStack volume record with storage pool association and protocol-specific details VolumeVO volumeVO = volumeDao.findById(volInfo.getId()); @@ -201,22 +200,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 +230,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 +295,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 +370,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) { @@ -412,7 +398,6 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore 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()); @@ -425,7 +410,6 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore 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 @@ -520,8 +504,8 @@ 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; + VolumeDetailVO lunDetail = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_NAME); + String lunName = lunDetail != null ? lunDetail.getValue() : null; if (lunName == null) { logger.warn("revokeAccessForVolume: No LUN name found for volume [{}]; skipping revoke", volumeVO.getId()); return; @@ -671,10 +655,9 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback 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 +220,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 +267,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()); + HashSet providedKeys = new HashSet<>(details.keySet()); if (!providedKeys.containsAll(requiredKeys)) { - Set missing = new java.util.HashSet<>(requiredKeys); + HashSet missing = new HashSet<>(requiredKeys); missing.removeAll(providedKeys); throw new CloudRuntimeException("ONTAP primary storage creation failed, missing detail(s): " + missing); } @@ -283,16 +287,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 +313,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 +365,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()); @@ -401,7 +378,6 @@ public boolean attachZone(DataStore dataStore, ZoneScope scope, Hypervisor.Hyper _dataStoreHelper.attachZone(dataStore); return true; } - private boolean validateProtocolSupportAndFetchHostsIdentifier(List hosts, ProtocolType protocolType, List hostIdentifiers) { switch (protocolType) { case ISCSI: @@ -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/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..1bbf54187a18 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 @@ -257,7 +257,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); @@ -341,7 +341,7 @@ public void deleteStorageVolume(Volume volume) { 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()); @@ -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..28f10de3abaa 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); @@ -298,22 +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 { @@ -405,25 +389,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..02661909d7e6 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; @@ -174,14 +175,14 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { logger.debug("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; @@ -196,7 +197,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 +219,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 +239,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { throw feignEx; } - logger.debug("createAccessGroup: createdIgroup: {}", createdIgroup); + logger.info("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); @@ -264,57 +264,59 @@ public void deleteAccessGroup(AccessGroup accessGroup) { 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(!CollectionUtils.isEmpty(accessGroup.getHostsToConnect())) { + for (HostVO host : accessGroup.getHostsToConnect()) { + String igroupName = OntapStorageUtils.getIgroupName(svmName, host.getName()); + logger.info("deleteAccessGroup: iGroup name '{}'", igroupName); - 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); + // 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) { @@ -348,13 +350,13 @@ public AccessGroup getAccessGroup(Map values) { logger.debug("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()); @@ -387,14 +389,14 @@ public Map enableLogicalAccess(Map values) { Map response = 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 @@ -504,8 +506,8 @@ 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; + Integer lunLogicalUnitNum = lunMapResponse.getRecords().get(0).getLogicalUnitNumber(); + String lunNumber = lunLogicalUnitNum != null ? lunLogicalUnitNum.toString() : null; return lunNumber != null ? Map.of(OntapStorageConstants.LOGICAL_UNIT_NUMBER, lunNumber) : null; } } catch (Exception e) { 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..88dd70db2972 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 @@ -63,26 +63,36 @@ 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..bc54ca5eb3ac 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 @@ -42,7 +42,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..cdf49b1db80d 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 @@ -293,8 +293,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 +304,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 +326,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 +337,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 +371,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 +402,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 +427,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 +458,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 +475,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 +557,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 +743,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 +777,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 +845,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/service/StorageStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/StorageStrategyTest.java index c2a4b56a1fa1..b859f57b37b1 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 @@ -121,29 +121,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,24 +150,21 @@ 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; } 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..77ca485d8f63 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; @@ -49,6 +48,7 @@ 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; @@ -107,6 +107,7 @@ class OntapVMSnapshotStrategyTest { private static final String VM_UUID = "vm-uuid-123"; @Spy + @InjectMocks private OntapVMSnapshotStrategy strategy; @Mock @@ -133,34 +134,8 @@ class OntapVMSnapshotStrategyTest { 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); + void setUp() { + // @InjectMocks handles injection into inherited fields } // ────────────────────────────────────────────────────────────────────────── 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..029c36a98fc0 100644 --- a/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/snapshot/VMSnapshotManagerImpl.java @@ -135,6 +135,7 @@ public class VMSnapshotManagerImpl extends MutualExclusiveIdsManagerBase implements VMSnapshotManager, VMSnapshotService, VmWorkJobHandler, Configurable { public static final String VM_WORK_JOB_HANDLER = VMSnapshotManagerImpl.class.getSimpleName(); + public static final String ONTAP_PLUGIN_NAME = "NetApp ONTAP"; @Inject VMInstanceDao _vmInstanceDao; @@ -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())) { + 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