From 9ab7ec50bfb09d7b6e66b130fd049cdf028983db Mon Sep 17 00:00:00 2001
From: "Jain, Rajiv"
Date: Thu, 24 Sep 2026 20:34:29 +0530
Subject: [PATCH 1/9] ONTAP: cache templates on primary storage and clone
volumes from them
Enable the managed-storage template cache flow for the ONTAP primary
storage plugin so that VM root volumes are cloned from a template copy
kept on the pool's FlexVolume instead of being copied from secondary
storage for every deployment.
- Advertise CAN_CREATE_VOLUME_FROM_VOLUME from the datastore driver.
- Create the template cache on primary: an iSCSI LUN (identity tracked on
template_spool_ref) or a qcow2 file for NFS, populated by the KVM host.
- Clone new volumes from the cached template: LUN clone for iSCSI and
ONTAP file clone (FileCloneRequest) for NFS, growing the clone when the
disk offering is larger than the template.
- Clean up the cached LUN/file when the template is removed from the pool;
a missing file is treated as already deleted.
- Move protocol-specific create/clone/resize logic into
UnifiedSANStrategy and UnifiedNASStrategy, and add a StorageStrategy
helper to poll an optional async job.
- Add unit tests for the driver and the NAS/SAN strategies.
---
.../driver/OntapPrimaryDatastoreDriver.java | 589 +++++++++++-
.../storage/feign/client/NASFeignClient.java | 11 +
.../storage/feign/client/SANFeignClient.java | 4 +-
.../storage/feign/model/FileCloneRequest.java | 131 +++
.../cloudstack/storage/feign/model/Lun.java | 2 +
.../storage/service/StorageStrategy.java | 62 +-
.../storage/service/UnifiedNASStrategy.java | 137 ++-
.../storage/service/UnifiedSANStrategy.java | 182 +++-
.../storage/utils/OntapStorageConstants.java | 11 +
.../storage/utils/OntapStorageUtils.java | 48 -
.../OntapPrimaryDatastoreDriverTest.java | 853 +++++++++++++++++-
.../storage/service/StorageStrategyTest.java | 13 +-
.../service/UnifiedNASStrategyTest.java | 223 +++++
.../service/UnifiedSANStrategyTest.java | 402 ++++++++-
14 files changed, 2534 insertions(+), 134 deletions(-)
create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
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 ece29f7cd0ac..d4d64efccada 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
@@ -32,8 +32,10 @@
import com.cloud.storage.VolumeDetailVO;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.ScopeType;
+import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.dao.SnapshotDetailsDao;
import com.cloud.storage.dao.SnapshotDetailsVO;
+import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.Pair;
@@ -44,10 +46,13 @@
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreCapabilities;
+import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreDriver;
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
+import org.apache.commons.lang3.StringUtils;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
@@ -55,19 +60,22 @@
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
+import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot;
import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.feign.model.LunSpace;
+import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.SANStrategy;
import org.apache.cloudstack.storage.service.StorageStrategy;
+import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
-import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -91,6 +99,7 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
@Inject private VolumeDao volumeDao;
@Inject private VolumeDetailsDao volumeDetailsDao;
@Inject private SnapshotDetailsDao snapshotDetailsDao;
+ @Inject private VMTemplatePoolDao vmTemplatePoolDao;
@Override
public Map getCapabilities() {
@@ -98,6 +107,9 @@ public Map getCapabilities() {
Map mapCapabilities = new HashMap<>();
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
+ // Enables the framework to cache a template on the FlexVolume once and serve every later
+ // deployment with an array-side clone instead of another copy from secondary storage.
+ mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString());
return mapCapabilities;
}
@@ -151,24 +163,25 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
// 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);
+ // Create the backend storage object: a clone of the cached template when the
+ // orchestrator asked for one, otherwise a blank LUN (iSCSI) or qcow2 file (NFS).
+ Long cloneOfTemplateId = getTemplateIdForCloning(volInfo.getId());
+ CloudStackVolume clonedCloudStackVolume = cloneOfTemplateId != null
+ ? cloneCloudStackVolumeFromTemplate(storagePool, volInfo, details, cloneOfTemplateId)
+ : createCloudStackVolume(storagePool, volInfo, details);
volumeVO.setPoolType(storagePool.getPoolType());
volumeVO.setPoolId(storagePool.getId());
if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
- String lunName = created != null && created.getLun() != null ? created.getLun().getName() : null;
- if (lunName == null) {
- throw new CloudRuntimeException("Missing LUN name for volume " + volInfo.getId());
- }
+ // createCloudStackVolume validates the Feign response (LUN name + uuid) before returning
+ Lun createdLun = clonedCloudStackVolume.getLun();
+ String lunName = createdLun.getName();
// Persist LUN details for future operations (delete, grant/revoke access)
- volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_UUID, created.getLun().getUuid(), false);
+ volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_UUID, createdLun.getUuid(), false);
volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_NAME, lunName, false);
- if (created.getLun().getUuid() != null) {
- volumeVO.setFolder(created.getLun().getUuid());
- }
+ volumeVO.setFolder(createdLun.getUuid());
logger.info("createAsync: Created LUN [{}] for volume [{}]. LUN mapping will occur during grantAccess() to per-host igroup.",
lunName, volumeVO.getId());
@@ -180,6 +193,8 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
}
volumeDao.update(volumeVO.getId(), volumeVO);
}
+ } else if (dataObject.getType() == DataObjectType.TEMPLATE) {
+ createCmdResult = createTemplateOnPrimary(storagePool, (TemplateInfo) dataObject, details);
} else {
errMsg = "Invalid DataObjectType (" + dataObject.getType() + ") passed to createAsync";
logger.error(errMsg);
@@ -203,8 +218,217 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
*/
private CloudStackVolume createCloudStackVolume(StoragePoolVO storagePool, VolumeInfo volumeObject, Map details) {
StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
- CloudStackVolume cloudStackVolumeRequest = OntapStorageUtils.createCloudStackVolumeRequestByProtocol(storagePool, details, volumeObject);
- return storageStrategy.createCloudStackVolume(cloudStackVolumeRequest);
+ return storageStrategy.createCloudStackVolume(createVolumeRequest(storagePool, details, volumeObject));
+ }
+
+ /**
+ * Creates the backend object that caches a template on this pool's FlexVolume.
+ *
+ * Protocol-specific work is delegated to {@link StorageStrategy#createTemplateCache}.
+ * This method maps the result to {@link CreateCmdResult} and records SAN identity on
+ * {@code template_spool_ref} ({@code local_download_path} = LUN uuid).
+ */
+ private CreateCmdResult createTemplateOnPrimary(StoragePoolVO storagePool, TemplateInfo templateInfo, Map details) {
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ long sizeInBytes = getDataObjectSizeIncludingHypervisorSnapshotReserve(templateInfo, storagePool);
+ CloudStackVolume created = null;
+ try {
+ created = storageStrategy.createTemplateCache(storagePool, templateInfo, details, sizeInBytes);
+ String path = recordTemplateCacheOnSpoolRef(storagePool, templateInfo, created, sizeInBytes);
+ return new CreateCmdResult(path, new Answer(null, true, null));
+ } catch (Exception e) {
+ // Compensating delete for SAN: strategy cleans create-time LUN failures; this covers
+ // post-create failures (e.g. template_spool_ref update) after a LUN was returned.
+ if (created != null && created.getLun() != null) {
+ bestEffortDeleteTemplateCacheLun(storageStrategy, details.get(OntapStorageConstants.SVM_NAME),
+ created.getLun().getName(), created.getLun().getUuid());
+ }
+ if (e instanceof CloudRuntimeException) {
+ throw (CloudRuntimeException) e;
+ }
+ throw new CloudRuntimeException("Failed to create template cache for template [" + templateInfo.getId()
+ + "]: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Persists SAN cache identity on {@code template_spool_ref} when a LUN was created.
+ * NFS returns the template uuid as the create path; {@code install_path} is filled later.
+ */
+ private String recordTemplateCacheOnSpoolRef(StoragePoolVO storagePool, TemplateInfo templateInfo,
+ CloudStackVolume created, long sizeInBytes) {
+ if (created == null || created.getLun() == null) {
+ return templateInfo.getUuid();
+ }
+ Lun lun = created.getLun();
+ VMTemplateStoragePoolVO templatePoolRef = findTemplatePoolRef(storagePool.getId(), templateInfo.getId());
+ templatePoolRef.setLocalDownloadPath(lun.getUuid());
+ templatePoolRef.setTemplateSize(sizeInBytes);
+ vmTemplatePoolDao.update(templatePoolRef.getId(), templatePoolRef);
+ return lun.getName();
+ }
+
+ /**
+ * Clones the cached template into a new volume on the same FlexVolume.
+ *
+ * Invoked when {@code StorageSystemDataMotionStrategy} has recorded a
+ * {@code cloneOfTemplate} detail on the volume.
+ */
+ private CloudStackVolume cloneCloudStackVolumeFromTemplate(StoragePoolVO storagePool, VolumeInfo volumeInfo,
+ Map details, long templateId) {
+ VMTemplateStoragePoolVO templatePoolRef = findTemplatePoolRef(storagePool.getId(), templateId);
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ boolean iscsi = isIscsi(details);
+
+ CloudStackVolume request = iscsi
+ ? createCloneLunRequest(storagePool, details, volumeInfo, templatePoolRef, templateId)
+ : createCloneFileRequest(storagePool, volumeInfo, templatePoolRef, templateId);
+
+ CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request);
+ // SAN cloneCloudStackVolume validates the Feign response (LUN name + uuid) before returning
+ if (cloned == null) {
+ throw new CloudRuntimeException("ONTAP returned nothing when cloning template [" + templateId
+ + "] for volume [" + volumeInfo.getId() + "]");
+ }
+
+ logger.info("cloneCloudStackVolumeFromTemplate: Cloned template [{}] for volume [{}] on pool [{}]",
+ templateId, volumeInfo.getId(), storagePool.getId());
+
+ long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool);
+ if (requestedSize > templatePoolRef.getTemplateSize()) {
+ logger.info("cloneCloudStackVolumeFromTemplate: Growing clone of template [{}] from {} to {} bytes for volume [{}]",
+ templateId, templatePoolRef.getTemplateSize(), requestedSize, volumeInfo.getId());
+ storageStrategy.resizeCloudStackVolume(cloned, requestedSize);
+ }
+
+ return cloned;
+ }
+
+ /**
+ * Returns the CloudStack template id the volume should be cloned from, or null for a blank volume.
+ *
+ * {@code StorageSystemDataMotionStrategy} persists this detail immediately before calling
+ * {@code createAsync} and removes it right after, so it is only visible during creation.
+ */
+ private Long getTemplateIdForCloning(long volumeId) {
+ VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeId, OntapStorageConstants.CLONE_OF_TEMPLATE);
+ if (detail == null || detail.getValue() == null || detail.getValue().isEmpty()) {
+ return null;
+ }
+ return Long.valueOf(detail.getValue());
+ }
+
+ private VMTemplateStoragePoolVO findTemplatePoolRef(long poolId, long templateId) {
+ VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(poolId, templateId, null);
+ if (templatePoolRef == null) {
+ throw new CloudRuntimeException("No template_spool_ref row for template [" + templateId + "] on pool [" + poolId + "]");
+ }
+ return templatePoolRef;
+ }
+
+ /**
+ * Deletes the LUN caching a template on this pool, invoked by template eviction
+ * ({@code TemplateManagerImpl.evictTemplateFromStoragePool}).
+ *
+ * Volumes previously cloned from this LUN are unaffected: an ONTAP sis-clone shares blocks
+ * with its source through reference counting rather than depending on it, so the source can be
+ * removed while its clones stay online.
+ *
+ * {@code deleteCloudStackVolume} already unmaps as it deletes ({@code allow_delete_while_mapped})
+ * and treats a missing LUN as success.
+ */
+ private void deleteTemplateOnPrimary(DataStore store, TemplateInfo templateInfo) {
+ StoragePoolVO storagePool = storagePoolDao.findById(store.getId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage Pool not found for id: " + store.getId());
+ }
+
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(store.getId());
+ VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(storagePool.getId(), templateInfo.getId(), null);
+ if (templatePoolRef == null) {
+ logger.warn("deleteTemplateOnPrimary: No template_spool_ref for template [{}] on pool [{}]; nothing to delete",
+ templateInfo.getId(), storagePool.getId());
+ return;
+ }
+
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ if (isIscsi(details)) {
+ deleteIscsiTemplateCache(storagePool, templateInfo, templatePoolRef, storageStrategy, details);
+ } else {
+ deleteNfsTemplateCache(details, templateInfo, templatePoolRef, storageStrategy);
+ }
+ }
+
+ private void deleteIscsiTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
+ VMTemplateStoragePoolVO templatePoolRef, StorageStrategy storageStrategy,
+ Map details) {
+ String lunName = getTemplateLunName(storagePool, templateInfo.getId());
+ String lunUuid = templatePoolRef.getLocalDownloadPath();
+ deleteTemplateCacheLun(storageStrategy, details.get(OntapStorageConstants.SVM_NAME), lunName, lunUuid);
+ logger.info("deleteIscsiTemplateCache: Deleted template cache LUN [{}] for template [{}] on pool [{}]",
+ lunName, templateInfo.getId(), storagePool.getId());
+ }
+
+ /**
+ * Deletes a template-cache LUN. When {@code lunUuid} is missing, resolves the LUN by its
+ * deterministic name so eviction and create-time rollback still work.
+ */
+ private void deleteTemplateCacheLun(StorageStrategy storageStrategy, String svmName, String lunName, String lunUuid) {
+ String uuid = lunUuid;
+ if (uuid == null || uuid.isEmpty()) {
+ CloudStackVolume existing = getCloudStackVolumeByName(storageStrategy, svmName, lunName);
+ if (existing == null || existing.getLun() == null || existing.getLun().getUuid() == null) {
+ logger.warn("deleteTemplateCacheLun: LUN [{}] not found on SVM [{}]; nothing to delete", lunName, svmName);
+ return;
+ }
+ uuid = existing.getLun().getUuid();
+ }
+
+ Lun lun = new Lun();
+ lun.setUuid(uuid);
+ lun.setName(lunName);
+
+ CloudStackVolume deleteRequest = new CloudStackVolume();
+ deleteRequest.setLun(lun);
+ storageStrategy.deleteCloudStackVolume(deleteRequest);
+ }
+
+ /**
+ * Best-effort cleanup of a template-cache LUN after a failed create. Never masks the original
+ * create failure.
+ */
+ private void bestEffortDeleteTemplateCacheLun(StorageStrategy storageStrategy, String svmName, String lunName,
+ String lunUuid) {
+ try {
+ deleteTemplateCacheLun(storageStrategy, svmName, lunName, lunUuid);
+ logger.info("bestEffortDeleteTemplateCacheLun: Removed leftover template cache LUN [{}] on SVM [{}]",
+ lunName, svmName);
+ } catch (Exception cleanupEx) {
+ logger.warn("bestEffortDeleteTemplateCacheLun: Failed to remove leftover template cache LUN [{}] on SVM [{}]: {}",
+ lunName, svmName, cleanupEx.getMessage());
+ }
+ }
+
+ private void deleteNfsTemplateCache(Map details, TemplateInfo templateInfo,
+ VMTemplateStoragePoolVO templatePoolRef, StorageStrategy storageStrategy) {
+ String filePath = templatePoolRef.getInstallPath();
+ if (filePath == null || filePath.isEmpty()) {
+ logger.warn("deleteNfsTemplateCache: No install_path recorded for template [{}]; nothing to delete",
+ templateInfo.getId());
+ return;
+ }
+ String flexVolUuid = details.get(OntapStorageConstants.VOLUME_UUID);
+ if (flexVolUuid == null || flexVolUuid.isEmpty()) {
+ // Misconfigured pool detail — fail eviction rather than calling ONTAP with a null
+ // volume UUID (which would hit /api/storage/volumes/null and still look like success
+ // upstream if we swallowed the error).
+ throw new CloudRuntimeException("FlexVolume UUID (volumeUUID) is missing from storage pool details; "
+ + "cannot delete NFS template cache file [" + filePath + "] for template ["
+ + templateInfo.getId() + "]");
+ }
+ ((UnifiedNASStrategy) storageStrategy).deleteFileByPath(flexVolUuid, filePath);
+ logger.info("deleteNfsTemplateCache: Deleted template cache file [{}] for template [{}]",
+ filePath, templateInfo.getId());
}
/**
@@ -236,6 +460,10 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac
logger.info("deleteAsync: Volume deleted: " + volumeInfo.getId());
commandResult.setResult(null);
commandResult.setSuccess(true);
+ } else if (data.getType() == DataObjectType.TEMPLATE) {
+ deleteTemplateOnPrimary(store, (TemplateInfo) data);
+ commandResult.setResult(null);
+ commandResult.setSuccess(true);
} else if (data.getType() == DataObjectType.SNAPSHOT) {
// Delete the ONTAP FlexVolume snapshot that was created by takeSnapshot
deleteOntapSnapshot((SnapshotInfo) data, commandResult);
@@ -399,6 +627,8 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
volumeVO.setPoolType(storagePool.getPoolType());
volumeVO.setPoolId(storagePool.getId());
volumeDao.update(volumeVO.getId(), volumeVO);
+ } else if (dataObject.getType() == DataObjectType.TEMPLATE) {
+ grantAccessTemplate((TemplateInfo) dataObject, host, dataStore, storagePool);
} else {
logger.error("Invalid DataObjectType (" + dataObject.getType() + ") passed to grantAccess");
throw new CloudRuntimeException("Invalid DataObjectType (" + dataObject.getType() + ") passed to grantAccess");
@@ -415,23 +645,93 @@ private void grantAccessIscsi(Host host, VolumeVO volumeVO, Map
UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details);
String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());
- // 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
+ ensureAccessGroupForHost(sanStrategy, host, storagePool, svmName, accessGroupName);
+
+ // Create or retrieve existing LUN mapping
+ String lunNumber = sanStrategy.ensureLunMapped(svmName, cloudStackVolumeName, accessGroupName);
+
+ // Update volume path if changed (e.g., after migration or re-mapping)
+ String iscsiPath = buildIscsiPath(storagePool, lunNumber);
+ if (volumeVO.getPath() == null || !volumeVO.getPath().equals(iscsiPath)) {
+ volumeVO.set_iScsiName(iscsiPath);
+ volumeVO.setPath(iscsiPath);
+ }
+ }
+
+ /**
+ * Maps the cached template LUN to the host so the KVM agent can write the image into it.
+ *
+ * Called by the framework from {@code copyTemplateToManagedTemplateVolume} just before it
+ * issues the {@code CopyCommand}. That method reads {@code managedStoreTarget} from the pool
+ * details before this call, when the LUN number does not exist yet, so the stale
+ * value is corrected here. The datastore details are re-read when the command is built, so the
+ * update lands in time.
+ */
+ private void grantAccessTemplate(TemplateInfo templateInfo, Host host, DataStore dataStore, StoragePoolVO storagePool) {
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ if (!ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
+ logger.debug("grantAccessTemplate: NFS template [{}], no igroup mapping required", templateInfo.getUuid());
+ return;
+ }
+
+ String svmName = details.get(OntapStorageConstants.SVM_NAME);
+ VMTemplateStoragePoolVO templatePoolRef = findTemplatePoolRef(storagePool.getId(), templateInfo.getId());
+ String lunName = getTemplateLunName(storagePool, templateInfo.getId());
+
+ UnifiedSANStrategy sanStrategy = (UnifiedSANStrategy) OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());
+
+ ensureAccessGroupForHost(sanStrategy, host, storagePool, svmName, accessGroupName);
+
+ boolean lunMapped = false;
+ try {
+ String lunNumber = sanStrategy.ensureLunMapped(svmName, lunName, accessGroupName);
+ lunMapped = true;
+ String iscsiPath = buildIscsiPath(storagePool, lunNumber);
+
+ templatePoolRef.setInstallPath(iscsiPath);
+ vmTemplatePoolDao.update(templatePoolRef.getId(), templatePoolRef);
+ refreshManagedStoreTarget(dataStore, iscsiPath);
+
+ logger.info("grantAccessTemplate: Mapped template cache LUN [{}] to igroup [{}] as [{}] for template [{}]",
+ lunName, accessGroupName, iscsiPath, templateInfo.getId());
+ } catch (RuntimeException e) {
+ // Undo only this call's side effect (map). Do not delete the cache LUN — create already succeeded.
+ if (lunMapped) {
+ try {
+ unmapLunFromHost(sanStrategy, svmName, lunName, accessGroupName, host);
+ } catch (Exception rollbackEx) {
+ logger.warn("grantAccessTemplate: Failed to unmap template cache LUN [{}] from igroup [{}] after grant failure: {}",
+ lunName, accessGroupName, rollbackEx.getMessage());
+ }
+ }
+ throw e;
+ }
+ }
+
+ /**
+ * Ensures an igroup containing this host's initiator exists on the SVM.
+ *
+ * The igroup may be absent even for a host that used the pool before, because LUN maps are
+ * created with {@code delete_on_unmap}, which lets ONTAP remove the igroup on its own.
+ */
+ private void ensureAccessGroupForHost(UnifiedSANStrategy sanStrategy, Host host, StoragePoolVO storagePool,
+ String svmName, String accessGroupName) {
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
+ if (accessGroup == null || accessGroup.getIgroup() == null) {
+ logger.info("ensureAccessGroupForHost: Igroup {} does not exist for the host {} : Need to create Igroup for the host ", accessGroupName, host.getName());
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());
+ } else {
+ logger.info("ensureAccessGroupForHost: 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
@@ -439,16 +739,28 @@ private void grantAccessIscsi(Host host, VolumeVO volumeVO, Map
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);
+ logger.info("ensureAccessGroupForHost: Igroup {} is present now with initiators {} ", accessGroup.getIgroup().getName(), accessGroup.getIgroup().getInitiators());
+ }
- // 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);
+ /**
+ * Builds the volume path the KVM agent expects for managed iSCSI: {@code //}.
+ */
+ private String buildIscsiPath(StoragePoolVO storagePool, String lunNumber) {
+ return OntapStorageConstants.SLASH + storagePool.getPath() + OntapStorageConstants.SLASH + lunNumber;
+ }
+
+ private void refreshManagedStoreTarget(DataStore dataStore, String iscsiPath) {
+ if (!(dataStore instanceof PrimaryDataStore)) {
+ return;
+ }
+ PrimaryDataStore primaryDataStore = (PrimaryDataStore) dataStore;
+ Map storeDetails = primaryDataStore.getDetails();
+ if (storeDetails == null) {
+ return;
}
+ Map updated = new HashMap<>(storeDetails);
+ updated.put(PrimaryDataStore.MANAGED_STORE_TARGET, iscsiPath);
+ primaryDataStore.setDetails(updated);
}
/**
@@ -485,6 +797,8 @@ public void revokeAccess(DataObject dataObject, Host host, DataStore dataStore)
throw new CloudRuntimeException("CloudStack Volume not found for id: " + dataObject.getId());
}
revokeAccessForVolume(storagePool, volumeVO, host);
+ } else if (dataObject.getType() == DataObjectType.TEMPLATE) {
+ revokeAccessForTemplate(storagePool, (TemplateInfo) dataObject, host);
} else {
logger.error("revokeAccess: Invalid DataObjectType (" + dataObject.getType() + ") passed to revokeAccess");
throw new CloudRuntimeException("Invalid DataObjectType (" + dataObject.getType() + ") passed to revokeAccess");
@@ -510,46 +824,73 @@ private void revokeAccessForVolume(StoragePoolVO storagePool, VolumeVO volumeVO,
// Retrieve LUN name from volume details; if missing, volume may not have been fully created
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, result.cloudStackVolume.getLun().getUuid());
- disableLogicalAccessMap.put(OntapStorageConstants.IGROUP_DOT_UUID, result.accessGroup.getIgroup().getUuid());
- storageStrategy.disableLogicalAccess(disableLogicalAccessMap);
+ String lunName = lunDetail != null ? lunDetail.getValue() : null;
+ if (lunName == null) {
+ logger.warn("revokeAccessForVolume: No LUN name found for volume [{}]; skipping revoke", volumeVO.getId());
+ return;
+ }
+ unmapLunFromHost(storageStrategy, svmName, lunName, accessGroupName, host);
+ }
+ }
- logger.info("revokeAccessForVolume: Successfully revoked access to LUN [{}] for host [{}]",
- result.lunName, host.getName());
+ /**
+ * Unmaps the cached template LUN once the framework has finished writing the image into it.
+ */
+ private void revokeAccessForTemplate(StoragePoolVO storagePool, TemplateInfo templateInfo, Host host) {
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ if (!ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
+ logger.debug("revokeAccessForTemplate: NFS template [{}], no igroup mapping to remove", templateInfo.getUuid());
+ return;
}
+
+ String svmName = details.get(OntapStorageConstants.SVM_NAME);
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ String accessGroupName = OntapStorageUtils.getIgroupName(svmName, host.getUuid());
+ String lunName = getTemplateLunName(storagePool, templateInfo.getId());
+
+ logger.info("revokeAccessForTemplate: Revoking access to template cache LUN [{}] for host [{}]", lunName, host.getName());
+ unmapLunFromHost(storageStrategy, svmName, lunName, accessGroupName, host);
}
- @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;
+ /**
+ * Removes a LUN-to-igroup mapping, skipping quietly when the LUN, the igroup or the host
+ * initiator is already gone.
+ */
+ private void unmapLunFromHost(StorageStrategy storageStrategy, String svmName, String lunName,
+ String accessGroupName, Host host) {
+ ValidateRevoke result = getValidateRevoke(lunName, host, storageStrategy, svmName, accessGroupName);
+ if (result == null) {
+ return;
}
+ Map disableLogicalAccessMap = new HashMap<>();
+ 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("unmapLunFromHost: Successfully revoked access to LUN [{}] for host [{}]", result.lunName, host.getName());
+ }
+
+ @Nullable
+ private ValidateRevoke getValidateRevoke(String lunName, Host host, StorageStrategy storageStrategy, String svmName, String accessGroupName) {
// 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());
+ logger.warn("getValidateRevoke: LUN [{}] not found on ONTAP, skipping revoke", lunName);
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);
+ logger.warn("getValidateRevoke: 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",
+ logger.warn("getValidateRevoke: Initiator [{}] is not in iGroup [{}], skipping revoke",
host.getStorageUrl(), accessGroupName);
return null;
}
@@ -600,14 +941,56 @@ private AccessGroup getAccessGroupByName(StorageStrategy storageStrategy, String
return accessGroup;
}
+ /**
+ * ONTAP is only supported with KVM, which does not take hypervisor-side snapshots into the
+ * volume itself, so no reserve is added on top of the requested size.
+ *
+ * For a template this returns the virtual size ({@code VMTemplateVO.size}), not the
+ * compressed size on secondary storage. The cached template LUN is written by the KVM agent
+ * with {@code qemu-img convert} from QCOW2 to RAW, so it must be able to hold the fully
+ * expanded image.
+ */
@Override
public long getDataObjectSizeIncludingHypervisorSnapshotReserve(DataObject dataObject, StoragePool storagePool) {
- return 0;
+ if (dataObject == null) {
+ return 0;
+ }
+ Long size = dataObject.getSize();
+ return size != null && size > 0 ? size : 0;
}
@Override
public long getBytesRequiredForTemplate(TemplateInfo templateInfo, StoragePool storagePool) {
- return 0;
+ if (templateInfo == null || storagePool == null) {
+ return 0;
+ }
+ // template_spool_ref is inserted in Allocated before the cache exists;
+ // only skip reservation when the template is Ready and has a backend identity.
+ VMTemplateStoragePoolVO templatePoolRef =
+ vmTemplatePoolDao.findByPoolTemplate(storagePool.getId(), templateInfo.getId(), null);
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ if (isTemplateCachedOnPool(templatePoolRef, details)) {
+ return 0;
+ }
+ return getDataObjectSizeIncludingHypervisorSnapshotReserve(templateInfo, storagePool);
+ }
+
+ /**
+ * Returns true when the primary template cache is present and usable for clone/deploy.
+ * A spool_ref row alone is not enough: CloudStack creates it in Allocated before the LUN/file exists.
+ * Ready is sufficient; downloadState is set alongside Ready on the managed-cache success path.
+ */
+ private boolean isTemplateCachedOnPool(VMTemplateStoragePoolVO templatePoolRef, Map details) {
+ if (templatePoolRef == null) {
+ return false;
+ }
+ if (templatePoolRef.getState() != ObjectInDataStoreStateMachine.State.Ready) {
+ return false;
+ }
+ if (details != null && isIscsi(details)) {
+ return StringUtils.isNotBlank(templatePoolRef.getLocalDownloadPath());
+ }
+ return StringUtils.isNotBlank(templatePoolRef.getInstallPath());
}
@Override
@@ -970,6 +1353,116 @@ private CloudStackVolume createDeleteCloudStackVolumeRequest(StoragePool storage
}
+ private boolean isIscsi(Map details) {
+ return ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL));
+ }
+
+ /**
+ * Builds the request that creates a blank volume (LUN for iSCSI, qcow2 file for NFS).
+ */
+ private CloudStackVolume createVolumeRequest(StoragePoolVO storagePool, Map details, DataObject volumeObject) {
+ CloudStackVolume request = new CloudStackVolume();
+ String protocol = details.get(OntapStorageConstants.PROTOCOL);
+ if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) {
+ request.setDatastoreId(String.valueOf(storagePool.getId()));
+ request.setVolumeInfo(volumeObject);
+ } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) {
+ Lun lunRequest = new Lun();
+ Svm svm = new Svm();
+ svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+ String lunName = volumeObject.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
+ if (!OntapStorageUtils.isValidName(lunName)) {
+ throw new InvalidParameterValueException("Invalid dataObject name [" + lunName
+ + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
+ }
+ lunRequest.setSvm(svm);
+ lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
+ lunRequest.setOsType(Lun.OsTypeEnum.valueOf(OntapStorageUtils.getOSTypeFromHypervisor(storagePool.getHypervisor().name())));
+ LunSpace lunSpace = new LunSpace();
+ lunSpace.setSize(volumeObject.getSize());
+ lunRequest.setSpace(lunSpace);
+ request.setLun(lunRequest);
+ } else {
+ throw new CloudRuntimeException("Unsupported protocol " + protocol);
+ }
+ return request;
+ }
+
+ /**
+ * LUN path used to cache a template on this pool: {@code /vol//cs_tmpl_}.
+ */
+ private String getTemplateLunName(StoragePoolVO storagePool, long templateId) {
+ return OntapStorageUtils.getLunName(storagePool.getName(), OntapStorageConstants.TEMPLATE_LUN_PREFIX + templateId);
+ }
+
+ /**
+ * Builds the request that clones the cached template LUN into a new volume LUN.
+ *
+ * Source identity mirrors the NFS file-clone path workflow: {@code clone.source.name} is
+ * the same deterministic ONTAP path used at template create
+ * ({@code /vol//cs_tmpl_}). {@code local_download_path} (LUN uuid) is
+ * still sent as a secondary identity.
+ *
+ * Size is omitted: ONTAP rejects a size on a clone create, and the clone inherits the
+ * source size. Growing to the requested volume size is a separate PATCH.
+ */
+ private CloudStackVolume createCloneLunRequest(StoragePoolVO storagePool, Map details,
+ VolumeInfo volumeObject, VMTemplateStoragePoolVO templatePoolRef,
+ long templateId) {
+ String sourceLunUuid = templatePoolRef.getLocalDownloadPath();
+ if (sourceLunUuid == null || sourceLunUuid.isEmpty()) {
+ throw new CloudRuntimeException("Template [" + templateId + "] has no cached LUN on pool ["
+ + storagePool.getId() + "]; cannot clone volume [" + volumeObject.getId() + "]");
+ }
+
+ Svm svm = new Svm();
+ svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+
+ String lunName = volumeObject.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
+ if (!OntapStorageUtils.isValidName(lunName)) {
+ throw new InvalidParameterValueException("Invalid dataObject name [" + lunName
+ + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
+ }
+
+ Lun.Source source = new Lun.Source();
+ source.setName(getTemplateLunName(storagePool, templateId));
+ source.setUuid(sourceLunUuid);
+ Lun.Clone clone = new Lun.Clone();
+ clone.setSource(source);
+
+ Lun lunRequest = new Lun();
+ lunRequest.setSvm(svm);
+ lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
+ lunRequest.setClone(clone);
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lunRequest);
+ return request;
+ }
+
+ /**
+ * Builds the request that clones the cached qcow2 ({@code install_path}) into a new file
+ * named after the volume uuid, inside the same FlexVolume.
+ */
+ private CloudStackVolume createCloneFileRequest(StoragePoolVO storagePool, VolumeInfo volumeInfo,
+ VMTemplateStoragePoolVO templatePoolRef, long templateId) {
+ String sourcePath = templatePoolRef.getInstallPath();
+ if (sourcePath == null || sourcePath.isEmpty()) {
+ throw new CloudRuntimeException("Template [" + templateId + "] has no cached file on pool ["
+ + storagePool.getId() + "]; cannot clone volume [" + volumeInfo.getId() + "]");
+ }
+
+ FileInfo file = new FileInfo();
+ file.setPath(sourcePath);
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setDatastoreId(String.valueOf(storagePool.getId()));
+ request.setVolumeInfo(volumeInfo);
+ request.setFile(file);
+ request.setDestinationPath(volumeInfo.getUuid());
+ return request;
+ }
+
// ──────────────────────────────────────────────────────────────────────────
// Snapshot Helper Methods
// ──────────────────────────────────────────────────────────────────────────
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
index 8cf21b94b2f1..ba38f57ef1c7 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/NASFeignClient.java
@@ -21,7 +21,9 @@
import feign.QueryMap;
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
+import org.apache.cloudstack.storage.feign.model.FileCloneRequest;
import org.apache.cloudstack.storage.feign.model.FileInfo;
+import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import feign.Headers;
import feign.Param;
@@ -58,6 +60,15 @@ void createFile(@Param("authHeader") String authHeader,
@Param("path") String filePath,
FileInfo file);
+ /**
+ * Creates a space-efficient clone of a file within a FlexVolume.
+ *
+ * ONTAP REST: {@code POST /api/storage/file/clone}
+ */
+ @RequestLine("POST /api/storage/file/clone")
+ @Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
+ JobResponse cloneFile(@Param("authHeader") String authHeader, FileCloneRequest request);
+
// Export Policy Operations
@RequestLine("POST /api/protocols/nfs/export-policies")
@Headers({"Authorization: {authHeader}"})
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
index 7281dc2ecbeb..d365468cee10 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
@@ -50,8 +50,8 @@ public interface SANFeignClient {
@Headers({"Authorization: {authHeader}"})
Lun getLunByUUID(@Param("authHeader") String authHeader, @Param("uuid") String uuid);
- @RequestLine("PATCH /{uuid}")
- @Headers({"Authorization: {authHeader}"})
+ @RequestLine("PATCH /api/storage/luns/{uuid}")
+ @Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun);
@RequestLine("DELETE /api/storage/luns/{uuid}")
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
new file mode 100644
index 000000000000..a9f2a106e9a8
--- /dev/null
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
@@ -0,0 +1,131 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cloudstack.storage.feign.model;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Request body for the ONTAP file clone API.
+ *
+ * ONTAP REST endpoint: {@code POST /api/storage/file/clone}
+ *
+ * Creates a space-efficient copy of a file. Source and destination paths are relative to the
+ * root of {@code volume}, and both must live in that same FlexVolume.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class FileCloneRequest {
+
+ @JsonProperty("volume")
+ private VolumeRef volume;
+
+ @JsonProperty("source_path")
+ private String sourcePath;
+
+ @JsonProperty("destination_path")
+ private String destinationPath;
+
+ @JsonProperty("overwrite_destination")
+ private Boolean overwriteDestination;
+
+ public FileCloneRequest() {
+ }
+
+ public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePath, String destinationPath) {
+ this.volume = new VolumeRef(flexVolUuid, flexVolName);
+ this.sourcePath = sourcePath;
+ this.destinationPath = destinationPath;
+ }
+
+ public VolumeRef getVolume() {
+ return volume;
+ }
+
+ public void setVolume(VolumeRef volume) {
+ this.volume = volume;
+ }
+
+ public String getSourcePath() {
+ return sourcePath;
+ }
+
+ public void setSourcePath(String sourcePath) {
+ this.sourcePath = sourcePath;
+ }
+
+ public String getDestinationPath() {
+ return destinationPath;
+ }
+
+ public void setDestinationPath(String destinationPath) {
+ this.destinationPath = destinationPath;
+ }
+
+ public Boolean getOverwriteDestination() {
+ return overwriteDestination;
+ }
+
+ public void setOverwriteDestination(Boolean overwriteDestination) {
+ this.overwriteDestination = overwriteDestination;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class VolumeRef {
+
+ @JsonProperty("uuid")
+ private String uuid;
+
+ @JsonProperty("name")
+ private String name;
+
+ public VolumeRef() {
+ }
+
+ public VolumeRef(String uuid, String name) {
+ this.uuid = uuid;
+ this.name = name;
+ }
+
+ public String getUuid() {
+ return uuid;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "FileCloneRequest{volume=" + (volume != null ? volume.getUuid() : null)
+ + ", sourcePath=" + sourcePath
+ + ", destinationPath=" + destinationPath + "}";
+ }
+}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
index 364790958c8a..922751c9a77a 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
@@ -308,6 +308,7 @@ private String toIndentedString(Object o) {
}
+ @JsonInclude(JsonInclude.Include.NON_NULL)
public static class Clone {
@JsonProperty("source")
private Source source = null;
@@ -319,6 +320,7 @@ public void setSource(Source source) {
}
}
+ @JsonInclude(JsonInclude.Include.NON_NULL)
public static class Source {
@JsonProperty("name")
private String name = null;
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 c13b255c67ea..64c208ccbd94 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
@@ -40,6 +40,7 @@
import org.apache.cloudstack.storage.feign.model.Volume;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
@@ -54,6 +55,8 @@
import java.util.Map;
import java.util.Objects;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
+
/**
* Storage Strategy represents the communication path for all the ONTAP storage options
*
@@ -495,6 +498,27 @@ public String getNetworkInterface() {
*/
abstract public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume);
+ /**
+ * Creates the protocol-specific backend object that caches a template on this pool.
+ *
+ * iSCSI creates an empty LUN ({@code /vol/<flexVol>/cs_tmpl_<id>}) sized to
+ * {@code sizeInBytes}. NFS is a no-op on the array: the KVM agent later writes the qcow2
+ * into the mounted FlexVolume.
+ *
+ * Returns a {@link CloudStackVolume} so the driver can map it to {@code CreateCmdResult}
+ * and update {@code template_spool_ref}. SAN populates {@code lun}; NAS returns an empty
+ * volume (no LUN / file yet).
+ *
+ * @param storagePool CloudStack primary storage pool (one FlexVolume)
+ * @param templateInfo template being cached
+ * @param details pool details (SVM, protocol, etc.)
+ * @param sizeInBytes virtual size for the cache object (required for SAN; ignored for NAS)
+ * @return created cache identity, or an empty {@link CloudStackVolume} when nothing is
+ * pre-created on the array
+ */
+ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
+ Map details, long sizeInBytes);
+
/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
* it is going to mimic
@@ -519,14 +543,28 @@ public String getNetworkInterface() {
abstract public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume);
/**
- * Method encapsulates the behavior based on the opted protocol in subclasses.
+ * Creates a space-efficient clone of an existing object inside the same FlexVolume.
* it is going to mimic
* cloneLun for iSCSI, FC protocols
* cloneFile for NFS3.0 and NFS4.1 protocols
* cloneNameSpace for Nvme/TCP and Nvme/FC protocol
- * @param cloudstackVolume the CloudStack volume to copy
+ *
+ * ONTAP requires the source and the destination to live in the same FlexVolume, which
+ * holds because a CloudStack primary storage pool maps one-to-one onto a FlexVolume.
+ *
+ * @param cloudstackVolume describes the clone to create; the source is carried in the
+ * protocol-specific clone reference (for SAN, {@code lun.clone.source})
+ * @return the created CloudStackVolume, populated with the backend identity of the clone
*/
- abstract public void copyCloudStackVolume(CloudStackVolume cloudstackVolume);
+ abstract public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume);
+
+ /**
+ * Grows an existing backend object to {@code sizeInBytes}.
+ *
+ * Needed after cloning a cached template, because a clone inherits the size of its source
+ * while the service offering may ask for a larger disk.
+ */
+ abstract public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes);
/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
@@ -694,4 +732,22 @@ public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeIn
}
return true;
}
+
+ /**
+ * Polls an ONTAP async job when the API response includes a job reference.
+ *
+ * When no job is returned (synchronous completion), the operation is treated as
+ * successful after HTTP 2xx.
+ *
+ * @param response ONTAP job response (may be null or without a job)
+ * @param operationName label for logging and error messages
+ */
+ public void pollJobIfPresent(JobResponse response, String operationName) {
+ if (response == null || response.getJob() == null || response.getJob().getUuid() == null) {
+ logger.debug("pollJobIfPresent: No async job returned for operation [{}], continuing without polling",
+ operationName);
+ return;
+ }
+ jobPollForSuccess(response.getJob().getUuid(), 60, 2000);
+ }
}
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 198957ca5db8..b14a4f0cb02f 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
@@ -20,6 +20,8 @@
package org.apache.cloudstack.storage.service;
import com.cloud.agent.api.Answer;
+import com.cloud.agent.api.storage.ResizeVolumeCommand;
+import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.host.HostVO;
import com.cloud.storage.Storage;
import com.cloud.storage.VolumeVO;
@@ -29,11 +31,15 @@
import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.storage.command.CreateObjectCommand;
import org.apache.cloudstack.storage.command.DeleteCommand;
+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.ExportPolicy;
import org.apache.cloudstack.storage.feign.model.ExportRule;
+import org.apache.cloudstack.storage.feign.model.FileCloneRequest;
import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
@@ -61,6 +67,7 @@ public class UnifiedNASStrategy extends NASStrategy {
@Inject private VolumeDao volumeDao;
@Inject private EndPointSelector epSelector;
@Inject private StoragePoolDetailsDao storagePoolDetailsDao;
+ @Inject private PrimaryDataStoreDao primaryDataStoreDao;
public UnifiedNASStrategy(OntapStorage ontapStorage) {
super(ontapStorage);
@@ -90,6 +97,18 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
}
}
+ /**
+ * NFS template cache: nothing is pre-created on the array. The KVM agent writes the qcow2
+ * into the mounted FlexVolume; the framework records {@code install_path} afterward.
+ */
+ @Override
+ public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
+ Map details, long sizeInBytes) {
+ logger.info("createTemplateCache: NFS pool [{}], template [{}] will be written directly to the mounted FlexVolume",
+ storagePool.getId(), templateInfo.getId());
+ return null;
+ }
+
@Override
CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
return null;
@@ -112,9 +131,103 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
}
}
+ /**
+ * Clones a file inside the FlexVolume using ONTAP's file clone API.
+ *
+ * The source is taken from {@code file.path} and the destination from
+ * {@code destinationPath}, both relative to the root of the FlexVolume backing the pool.
+ */
+ @Override
+ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ if (cloudstackVolume == null || cloudstackVolume.getFile() == null
+ || cloudstackVolume.getFile().getPath() == null || cloudstackVolume.getDestinationPath() == null) {
+ logger.error("cloneCloudStackVolume: File clone failed. Invalid request: {}", cloudstackVolume);
+ throw new CloudRuntimeException("Failed to clone file, invalid request");
+ }
+ if (cloudstackVolume.getDatastoreId() == null) {
+ throw new CloudRuntimeException("Failed to clone file, no datastore id in the request");
+ }
+
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(Long.parseLong(cloudstackVolume.getDatastoreId()));
+ String flexVolUuid = details.get(OntapStorageConstants.VOLUME_UUID);
+ String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
+ if (flexVolUuid == null || flexVolUuid.isEmpty()) {
+ throw new CloudRuntimeException("Failed to clone file, FlexVolume uuid is missing from pool details");
+ }
+ String sourcePath = cloudstackVolume.getFile().getPath();
+ String destinationPath = cloudstackVolume.getDestinationPath();
+
+ logger.info("cloneCloudStackVolume: Cloning file [{}] to [{}] in FlexVol [{}]", sourcePath, destinationPath, flexVolName);
+ try {
+ FileCloneRequest request = new FileCloneRequest(flexVolUuid, flexVolName, sourcePath, destinationPath);
+ JobResponse jobResponse = nasFeignClient.cloneFile(getAuthHeader(), request);
+ pollJobIfPresent(jobResponse, "clone file [" + sourcePath + "] to [" + destinationPath + "]");
+
+ updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), cloudstackVolume.getVolumeInfo());
+
+ FileInfo clonedFile = new FileInfo();
+ clonedFile.setPath(destinationPath);
+
+ CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
+ clonedCloudStackVolume.setFile(clonedFile);
+ clonedCloudStackVolume.setDatastoreId(cloudstackVolume.getDatastoreId());
+ clonedCloudStackVolume.setVolumeInfo(cloudstackVolume.getVolumeInfo());
+ return clonedCloudStackVolume;
+ } catch (FeignException e) {
+ logger.error("FeignException occurred while cloning file [{}], Status: {}, Exception: {}",
+ sourcePath, e.status(), e.getMessage());
+ throw new CloudRuntimeException("Failed to clone file: " + e.getMessage());
+ } catch (Exception e) {
+ logger.error("Exception occurred while cloning file [{}], Exception: {}", sourcePath, e.getMessage());
+ throw new CloudRuntimeException("Failed to clone file: " + e.getMessage());
+ }
+ }
+
+ /**
+ * Grows the cloned qcow2 to the requested size via a host-side {@code qemu-img resize}.
+ */
@Override
- public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes) {
+ if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null) {
+ logger.error("resizeCloudStackVolume: Resize failed. Invalid request: {}", cloudstackVolume);
+ throw new CloudRuntimeException("Failed to resize file, invalid request");
+ }
+ if (sizeInBytes <= 0) {
+ throw new CloudRuntimeException("Failed to resize file, invalid size " + sizeInBytes);
+ }
+ DataObject volumeInfo = cloudstackVolume.getVolumeInfo();
+ Answer answer = resizeVolumeOnKVMHost(volumeInfo, sizeInBytes);
+ if (answer == null || !answer.getResult()) {
+ String errMsg = answer != null ? answer.getDetails() : "Failed to resize qcow2 on KVM host";
+ logger.error("resizeCloudStackVolume: " + errMsg);
+ throw new CloudRuntimeException(errMsg);
+ }
+ logger.info("resizeCloudStackVolume: Resized volume [{}] to {} bytes", volumeInfo.getUuid(), sizeInBytes);
+ }
+
+ private Answer resizeVolumeOnKVMHost(DataObject volumeInfo, long sizeInBytes) {
+ VolumeObject volumeObject = (VolumeObject) volumeInfo;
+ VolumeVO volume = volumeDao.findById(volumeObject.getId());
+ if (volume == null) {
+ throw new CloudRuntimeException("Volume not found with id: " + volumeObject.getId());
+ }
+
+ StoragePoolVO storagePool = primaryDataStoreDao.findById(volume.getPoolId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage Pool not found for id: " + volume.getPoolId());
+ }
+
+ ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
+ volume.getSize(), sizeInBytes, false, null);
+ EndPoint ep = epSelector.select(volumeInfo);
+ if (ep == null) {
+ String errMsg = "No remote endpoint to send ResizeVolumeCommand, check if host is up";
+ logger.error(errMsg);
+ return new Answer(cmd, false, errMsg);
+ }
+ logger.info("resizeVolumeOnKVMHost: Sending command to endpoint: {}", ep.getHostAddr());
+ return ep.sendMessage(cmd);
}
@Override
@@ -409,6 +522,28 @@ private Answer deleteVolumeOnKVMHost(DataObject volumeInfo) {
}
}
+ /**
+ * Deletes a file from a FlexVolume, treating an already-absent file as success.
+ */
+ public void deleteFileByPath(String flexVolUuid, String filePath) {
+ logger.info("deleteFileByPath: Deleting file [{}] from FlexVol [{}]", filePath, flexVolUuid);
+ try {
+ nasFeignClient.deleteFile(getAuthHeader(), flexVolUuid, filePath);
+ logger.debug("deleteFileByPath: Deleted file [{}]", filePath);
+ } catch (FeignException e) {
+ if (e.status() == 404) {
+ logger.warn("deleteFileByPath: File [{}] does not exist (status 404), skipping deletion", filePath);
+ return;
+ }
+ logger.error("FeignException occurred while deleting file [{}], Status: {}, Exception: {}",
+ filePath, e.status(), e.getMessage());
+ throw new CloudRuntimeException("Failed to delete file: " + e.getMessage());
+ } catch (Exception e) {
+ logger.error("Exception occurred while deleting file [{}], Exception: {}", filePath, e.getMessage());
+ throw new CloudRuntimeException("Failed to delete file: " + e.getMessage());
+ }
+ }
+
private FileInfo getFile(String volumeUuid, String filePath) {
logger.info("Get File: {} for volume: {}", filePath, volumeUuid);
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 2b0e65f9f7ea..b9e32b081e4d 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
@@ -22,13 +22,16 @@
import com.cloud.host.HostVO;
import com.cloud.utils.exception.CloudRuntimeException;
import feign.FeignException;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Initiator;
import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.feign.model.LunMap;
+import org.apache.cloudstack.storage.feign.model.LunSpace;
import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
@@ -78,6 +81,7 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
throw new CloudRuntimeException("Failed to create Lun: " + cloudstackVolume.getLun().getName());
}
Lun lun = createdLun.getRecords().get(0);
+ validateCreatedLun(lun, cloudstackVolume.getLun().getName(), "createCloudStackVolume");
logger.debug("createCloudStackVolume: LUN created successfully. Lun: {}", lun);
CloudStackVolume createdCloudStackVolume = new CloudStackVolume();
@@ -87,12 +91,97 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
logger.error("FeignException occurred while creating LUN: {}, Status: {}, Exception: {}",
cloudstackVolume.getLun().getName(), e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
+ } catch (CloudRuntimeException e) {
+ throw e;
} catch (Exception e) {
logger.error("Exception occurred while creating LUN: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage());
throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
}
}
+ /**
+ * Creates an empty LUN that caches a template on this pool's FlexVolume.
+ *
+ * Sized to the template virtual disk size (what KVM writes after {@code qemu-img convert}
+ * to RAW). On create failure, best-effort deletes any leftover LUN before rethrowing.
+ */
+ @Override
+ public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
+ Map details, long sizeInBytes) {
+ if (sizeInBytes <= 0) {
+ throw new CloudRuntimeException("Unknown virtual size for template [" + templateInfo.getId()
+ + "]; cannot size the template LUN on pool [" + storagePool.getId() + "]");
+ }
+
+ CloudStackVolume request = buildTemplateLunRequest(storagePool, details, templateInfo.getId(), sizeInBytes);
+ String lunName = request.getLun().getName();
+ CloudStackVolume created = null;
+ try {
+ created = createCloudStackVolume(request);
+ Lun lun = created.getLun();
+ logger.info("createTemplateCache: Created template cache LUN [{}] (uuid [{}], {} bytes) on pool [{}] for template [{}]",
+ lun.getName(), lun.getUuid(), sizeInBytes, storagePool.getId(), templateInfo.getId());
+ return created;
+ } catch (Exception e) {
+ bestEffortDeleteTemplateCacheLun(details.get(OntapStorageConstants.SVM_NAME), lunName,
+ created != null && created.getLun() != null ? created.getLun().getUuid() : null);
+ if (e instanceof CloudRuntimeException) {
+ throw (CloudRuntimeException) e;
+ }
+ throw new CloudRuntimeException("Failed to create template cache LUN for template [" + templateInfo.getId()
+ + "]: " + e.getMessage(), e);
+ }
+ }
+
+ private CloudStackVolume buildTemplateLunRequest(StoragePoolVO storagePool, Map details,
+ long templateId, long sizeInBytes) {
+ Svm svm = new Svm();
+ svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+
+ Lun lunRequest = new Lun();
+ lunRequest.setSvm(svm);
+ lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(),
+ OntapStorageConstants.TEMPLATE_LUN_PREFIX + templateId));
+ lunRequest.setOsType(Lun.OsTypeEnum.valueOf(
+ OntapStorageUtils.getOSTypeFromHypervisor(storagePool.getHypervisor().name())));
+ LunSpace lunSpace = new LunSpace();
+ lunSpace.setSize(sizeInBytes);
+ lunRequest.setSpace(lunSpace);
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lunRequest);
+ return request;
+ }
+
+ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, String lunUuid) {
+ try {
+ String uuid = lunUuid;
+ if (uuid == null || uuid.isEmpty()) {
+ Map lookup = Map.of(
+ OntapStorageConstants.NAME, lunName,
+ OntapStorageConstants.SVM_DOT_NAME, svmName);
+ CloudStackVolume existing = getCloudStackVolume(lookup);
+ if (existing == null || existing.getLun() == null || existing.getLun().getUuid() == null) {
+ logger.warn("bestEffortDeleteTemplateCacheLun: LUN [{}] not found on SVM [{}]; nothing to delete",
+ lunName, svmName);
+ return;
+ }
+ uuid = existing.getLun().getUuid();
+ }
+ Lun lun = new Lun();
+ lun.setUuid(uuid);
+ lun.setName(lunName);
+ CloudStackVolume deleteRequest = new CloudStackVolume();
+ deleteRequest.setLun(lun);
+ deleteCloudStackVolume(deleteRequest);
+ logger.info("bestEffortDeleteTemplateCacheLun: Removed leftover template cache LUN [{}] on SVM [{}]",
+ lunName, svmName);
+ } catch (Exception cleanupEx) {
+ logger.warn("bestEffortDeleteTemplateCacheLun: Failed to remove leftover template cache LUN [{}] on SVM [{}]: {}",
+ lunName, svmName, cleanupEx.getMessage());
+ }
+ }
+
@Override
CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
return null;
@@ -124,8 +213,99 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
}
}
+ /**
+ * Clones a LUN inside the FlexVolume using ONTAP's {@code clone.source} form of LUN create.
+ *
+ * The resulting LUN shares blocks with its source and is created in constant time.
+ * It is a sis-clone, so it stays readable after the source LUN is deleted.
+ */
+ @Override
+ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ if (cloudstackVolume == null || cloudstackVolume.getLun() == null) {
+ logger.error("cloneCloudStackVolume: LUN clone failed. Invalid request: {}", cloudstackVolume);
+ throw new CloudRuntimeException("Failed to clone Lun, invalid request");
+ }
+ Lun lunRequest = cloudstackVolume.getLun();
+ if (lunRequest.getClone() == null || lunRequest.getClone().getSource() == null) {
+ logger.error("cloneCloudStackVolume: LUN clone failed. No clone source in request for Lun {}", lunRequest.getName());
+ throw new CloudRuntimeException("Failed to clone Lun, no clone source provided");
+ }
+ logger.trace("cloneCloudStackVolume: Cloning Lun {} from source name={} uuid={}",
+ lunRequest.getName(),
+ lunRequest.getClone().getSource().getName(),
+ lunRequest.getClone().getSource().getUuid());
+ try {
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ OntapResponse clonedLun = sanFeignClient.createLun(authHeader, true, lunRequest);
+ if (clonedLun == null || CollectionUtils.isEmpty(clonedLun.getRecords())) {
+ logger.error("cloneCloudStackVolume: LUN clone returned no records for Lun {}", lunRequest.getName());
+ throw new CloudRuntimeException("Failed to clone Lun: " + lunRequest.getName());
+ }
+ Lun lun = clonedLun.getRecords().get(0);
+ validateCreatedLun(lun, lunRequest.getName(), "cloneCloudStackVolume");
+ logger.debug("cloneCloudStackVolume: LUN cloned successfully. Lun: {}", lun);
+
+ CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
+ clonedCloudStackVolume.setLun(lun);
+ return clonedCloudStackVolume;
+ } catch (FeignException e) {
+ logger.error("FeignException occurred while cloning LUN: {}, Status: {}, Exception: {}",
+ lunRequest.getName(), e.status(), e.getMessage());
+ throw new CloudRuntimeException("Failed to clone Lun: " + e.getMessage());
+ } catch (CloudRuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ logger.error("Exception occurred while cloning LUN: {}, Exception: {}", lunRequest.getName(), e.getMessage());
+ throw new CloudRuntimeException("Failed to clone Lun: " + e.getMessage());
+ }
+ }
+
+
+ /**
+ * Ensures ONTAP returned a usable LUN identity from create/clone. Callers in the datastore
+ * driver rely on non-null name and uuid, so reject incomplete records at the Feign boundary.
+ */
+ private void validateCreatedLun(Lun lun, String requestName, String operation) {
+ if (lun == null || lun.getName() == null || lun.getUuid() == null) {
+ logger.error("{}: ONTAP returned incomplete LUN for {}", operation, requestName);
+ throw new CloudRuntimeException("ONTAP returned incomplete LUN for: " + requestName);
+ }
+ }
+
+ /**
+ * Grows an existing LUN to {@code sizeInBytes}.
+ *
+ * Needed after cloning a cached template, because a clone inherits the size of its source
+ * while the service offering may ask for a larger disk.
+ */
@Override
- public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) {}
+ public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes) {
+ if (cloudstackVolume == null || cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) {
+ logger.error("resizeCloudStackVolume: Lun resize failed. Invalid request: {}", cloudstackVolume);
+ throw new CloudRuntimeException("Failed to resize Lun, invalid request");
+ }
+ if (sizeInBytes <= 0) {
+ throw new CloudRuntimeException("Failed to resize Lun, invalid size " + sizeInBytes);
+ }
+ String lunUuid = cloudstackVolume.getLun().getUuid();
+ logger.trace("resizeCloudStackVolume: Resizing Lun {} to {} bytes", lunUuid, sizeInBytes);
+ try {
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ LunSpace lunSpace = new LunSpace();
+ lunSpace.setSize(sizeInBytes);
+ Lun patch = new Lun();
+ patch.setSpace(lunSpace);
+ sanFeignClient.updateLun(authHeader, lunUuid, patch);
+ logger.debug("resizeCloudStackVolume: Lun {} resized to {} bytes", lunUuid, sizeInBytes);
+ } catch (FeignException e) {
+ logger.error("FeignException occurred while resizing LUN: {}, Status: {}, Exception: {}",
+ lunUuid, e.status(), e.getMessage());
+ throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ } catch (Exception e) {
+ logger.error("Exception occurred while resizing LUN: {}, Exception: {}", lunUuid, e.getMessage());
+ throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ }
+ }
@Override
public CloudStackVolume getCloudStackVolume(Map values) {
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 e5224237e526..51205069ee2a 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
@@ -109,4 +109,15 @@ public class OntapStorageConstants {
/** vm_snapshot_details key for ONTAP FlexVolume-level VM snapshots. */
public static final String ONTAP_FLEXVOL_SNAPSHOT = "ontapFlexVolSnapshot";
+
+ /** Name prefix of the LUN that caches a template on the FlexVol, suffixed with the template id. */
+ public static final String TEMPLATE_LUN_PREFIX = "cs_tmpl_";
+
+ /**
+ * Key of the {@code volume_details} row that {@code StorageSystemDataMotionStrategy} writes
+ * immediately before {@code createAsync} when a volume is to be cloned from a template already
+ * cached on this pool. The value is the CloudStack template id. The literal must stay in sync
+ * with the string used by the orchestrator.
+ */
+ public static final String CLONE_OF_TEMPLATE = "cloneOfTemplate";
}
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 8a74e77b3371..6cc06d75c8ad 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
@@ -19,18 +19,12 @@
package org.apache.cloudstack.storage.utils;
-import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.StringUtils;
import com.cloud.utils.exception.CloudRuntimeException;
-import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
-import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Lun;
-import org.apache.cloudstack.storage.feign.model.LunSpace;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
-import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.provider.StorageProviderFactory;
import org.apache.cloudstack.storage.service.StorageStrategy;
-import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -57,48 +51,6 @@ public static String generateAuthHeader (String username, String password) {
return BASIC + StringUtils.SPACE + new String(encodedBytes);
}
- public static CloudStackVolume createCloudStackVolumeRequestByProtocol(StoragePoolVO storagePool, Map details, DataObject volumeObject) {
- CloudStackVolume cloudStackVolumeRequest = null;
-
- String protocol = details.get(OntapStorageConstants.PROTOCOL);
- ProtocolType protocolType = ProtocolType.valueOf(protocol);
- switch (protocolType) {
- case NFS3:
- cloudStackVolumeRequest = new CloudStackVolume();
- cloudStackVolumeRequest.setDatastoreId(String.valueOf(storagePool.getId()));
- cloudStackVolumeRequest.setVolumeInfo(volumeObject);
- break;
- case ISCSI:
- Svm svm = new Svm();
- svm.setName(details.get(OntapStorageConstants.SVM_NAME));
- cloudStackVolumeRequest = new CloudStackVolume();
- Lun lunRequest = new Lun();
- lunRequest.setSvm(svm);
-
- LunSpace lunSpace = new LunSpace();
- lunSpace.setSize(volumeObject.getSize());
- lunRequest.setSpace(lunSpace);
- //Lun name is full path like in unified "/vol/VolumeName/LunName"
- String lunName = volumeObject.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
- if(!isValidName(lunName)) {
- String errMsg = "createAsync: Invalid dataObject name [" + lunName + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.";
- throw new InvalidParameterValueException(errMsg);
- }
- String lunFullName = getLunName(storagePool.getName(), lunName);
- lunRequest.setName(lunFullName);
-
- String osType = getOSTypeFromHypervisor(storagePool.getHypervisor().name());
- lunRequest.setOsType(Lun.OsTypeEnum.valueOf(osType));
-
- cloudStackVolumeRequest.setLun(lunRequest);
- break;
- default:
- throw new CloudRuntimeException("Unsupported protocol " + protocol);
-
- }
- return cloudStackVolumeRequest;
- }
-
public static boolean isValidName(String name) {
// Check for null and length constraint first
if (name == null || name.length() > 200) {
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 3c139e23cb88..9b210388bde2 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
@@ -21,15 +21,21 @@
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
+import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.ScopeType;
import com.cloud.storage.Storage;
+import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
@@ -38,6 +44,7 @@
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
@@ -56,6 +63,7 @@
import java.util.HashMap;
import java.util.Map;
+import static com.cloud.agent.api.to.DataObjectType.TEMPLATE;
import static com.cloud.agent.api.to.DataObjectType.VOLUME;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -64,10 +72,14 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
@@ -89,12 +101,21 @@ class OntapPrimaryDatastoreDriverTest {
@Mock
private VolumeDetailsDao volumeDetailsDao;
+ @Mock
+ private VMTemplatePoolDao vmTemplatePoolDao;
+
+ @Mock
+ private VMTemplateStoragePoolVO templatePoolRef;
+
@Mock
private DataStore dataStore;
@Mock
private VolumeInfo volumeInfo;
+ @Mock
+ private TemplateInfo templateInfo;
+
@Mock
private StoragePoolVO storagePool;
@@ -107,6 +128,9 @@ class OntapPrimaryDatastoreDriverTest {
@Mock
private UnifiedSANStrategy sanStrategy;
+ @Mock
+ private UnifiedNASStrategy nasStrategy;
+
@Mock
private AsyncCompletionCallback createCallback;
@@ -134,6 +158,7 @@ void testGetCapabilities() {
// so StorageSystemSnapshotStrategy handles snapshot backup to secondary storage
assertEquals(Boolean.TRUE.toString(), capabilities.get("STORAGE_SYSTEM_SNAPSHOT"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_SNAPSHOT"));
+ assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_VOLUME"));
}
@Test
@@ -165,7 +190,9 @@ void testCreateAsync_VolumeWithISCSI_Success() {
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
- when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
@@ -174,18 +201,12 @@ void testCreateAsync_VolumeWithISCSI_Success() {
Lun mockLun = new Lun();
mockLun.setName("/vol/vol1/lun1");
mockLun.setUuid("lun-uuid-123");
- // Create request volume (returned by Utility.createCloudStackVolumeRequestByProtocol)
- CloudStackVolume requestVolume = new CloudStackVolume();
- requestVolume.setLun(mockLun);
- // Create response volume (returned by sanStrategy.createCloudStackVolume)
CloudStackVolume responseVolume = new CloudStackVolume();
responseVolume.setLun(mockLun);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
.thenReturn(sanStrategy);
- utilityMock.when(() -> OntapStorageUtils.createCloudStackVolumeRequestByProtocol(
- any(), any(), any())).thenReturn(requestVolume);
when(sanStrategy.createCloudStackVolume(any())).thenReturn(responseVolume);
// Execute
@@ -225,13 +246,11 @@ void testCreateAsync_VolumeWithNFS_Success() {
CloudStackVolume mockCloudStackVolume = new CloudStackVolume();
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
- .thenReturn(sanStrategy);
- utilityMock.when(() -> OntapStorageUtils.createCloudStackVolumeRequestByProtocol(
- any(), any(), any())).thenReturn(mockCloudStackVolume);
+ .thenReturn(nasStrategy);
- when(sanStrategy.createCloudStackVolume(any())).thenReturn(mockCloudStackVolume);
+ when(nasStrategy.createCloudStackVolume(any())).thenReturn(mockCloudStackVolume);
// Execute
driver.createAsync(dataStore, volumeInfo, createCallback);
@@ -247,6 +266,32 @@ void testCreateAsync_VolumeWithNFS_Success() {
}
}
+ @Test
+ void testCreateAsync_KvmUnsupportedProtocol_FailsWithError() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, "FC");
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Unsupported protocol FC"));
+ }
+ }
+
@Test
void testDeleteAsync_NullStore_ThrowsException() {
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
@@ -275,7 +320,7 @@ void testDeleteAsync_ISCSIVolume_Success() {
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
@@ -318,6 +363,93 @@ void testDeleteAsync_NFSVolume_Success() {
// NFS deletion doesn't fail, handled by hypervisor
}
+ @Test
+ void testDeleteAsync_Template_DeletesCacheLun() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getLocalDownloadPath()).thenReturn("template-lun-uuid");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
+
+ driver.deleteAsync(dataStore, templateInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).deleteCloudStackVolume(requestCaptor.capture());
+ assertEquals("template-lun-uuid", requestCaptor.getValue().getLun().getUuid());
+ }
+ }
+
+
+ @Test
+ void testDeleteAsync_Template_ResolvesLunByNameWhenUuidMissing() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getLocalDownloadPath()).thenReturn(null);
+
+ Lun templateLun = new Lun();
+ templateLun.setName("/vol/vol1/cs_tmpl_50");
+ templateLun.setUuid("resolved-lun-uuid");
+ CloudStackVolume existing = new CloudStackVolume();
+ existing.setLun(templateLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
+ when(sanStrategy.getCloudStackVolume(argThat(map ->
+ map != null && "/vol/vol1/cs_tmpl_50".equals(map.get("name")))))
+ .thenReturn(existing);
+ doNothing().when(sanStrategy).deleteCloudStackVolume(any());
+
+ driver.deleteAsync(dataStore, templateInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).deleteCloudStackVolume(deleteCaptor.capture());
+ assertEquals("resolved-lun-uuid", deleteCaptor.getValue().getLun().getUuid());
+ assertEquals("/vol/vol1/cs_tmpl_50", deleteCaptor.getValue().getLun().getName());
+ }
+ }
+
+ @Test
+ void testDeleteAsync_Template_NoCachedLun_SucceedsWithoutCallingOntap() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
+
+ driver.deleteAsync(dataStore, templateInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).deleteCloudStackVolume(any());
+ }
+
@Test
void testGrantAccess_NullParameters_ThrowsException() {
assertThrows(CloudRuntimeException.class,
@@ -359,7 +491,7 @@ void testGrantAccess_ClusterScope_Success() {
existingIgroup.setName("igroup1");
existingAccessGroup.setIgroup(existingIgroup);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
utilityMock.when(() -> OntapStorageUtils.getIgroupName(anyString(), anyString()))
@@ -410,7 +542,7 @@ void testGrantAccess_IgroupNotFound_CreatesNewIgroup() {
createdIgroup.setName("igroup1");
createdAccessGroup.setIgroup(createdIgroup);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
utilityMock.when(() -> OntapStorageUtils.getIgroupName(anyString(), anyString()))
@@ -435,12 +567,12 @@ void testGrantAccess_IgroupNotFound_CreatesNewIgroup() {
@Test
void testRevokeAccess_NFSVolume_SkipsRevoke() {
// Setup - NFS volumes have no LUN mapping, so revokeAccess is a no-op
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
when(dataStore.getId()).thenReturn(1L);
when(volumeInfo.getType()).thenReturn(VOLUME);
when(volumeInfo.getId()).thenReturn(100L);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
when(volumeVO.getName()).thenReturn("test-volume");
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
@@ -449,7 +581,7 @@ void testRevokeAccess_NFSVolume_SkipsRevoke() {
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(host.getName()).thenReturn("host1");
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
@@ -496,7 +628,7 @@ void testRevokeAccess_ISCSIVolume_Success() {
AccessGroup mockAccessGroup = new AccessGroup();
mockAccessGroup.setIgroup(mockIgroup);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(sanStrategy);
utilityMock.when(() -> OntapStorageUtils.getIgroupName(anyString(), anyString()))
@@ -538,6 +670,529 @@ void testRevokeAccess_ISCSIVolume_Success() {
}
}
+ @Test
+ void testGetDataObjectSizeIncludingHypervisorSnapshotReserve_NoReserveAdded() {
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+
+ assertEquals(5368709120L, driver.getDataObjectSizeIncludingHypervisorSnapshotReserve(templateInfo, storagePool));
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_AlreadyCached_ReturnsZero() {
+ when(storagePool.getId()).thenReturn(1L);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getState()).thenReturn(ObjectInDataStoreStateMachine.State.Ready);
+ when(templatePoolRef.getLocalDownloadPath()).thenReturn("template-lun-uuid");
+
+ assertEquals(0L, driver.getBytesRequiredForTemplate(templateInfo, storagePool));
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_NotCached_ReturnsVirtualSize() {
+ when(storagePool.getId()).thenReturn(1L);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
+
+ assertEquals(5368709120L, driver.getBytesRequiredForTemplate(templateInfo, storagePool));
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_SpoolRefNotReady_ReturnsVirtualSize() {
+ when(storagePool.getId()).thenReturn(1L);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getState()).thenReturn(ObjectInDataStoreStateMachine.State.Allocated);
+
+ assertEquals(5368709120L, driver.getBytesRequiredForTemplate(templateInfo, storagePool));
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_ReadyWithoutBackendIdentity_ReturnsVirtualSize() {
+ when(storagePool.getId()).thenReturn(1L);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getState()).thenReturn(ObjectInDataStoreStateMachine.State.Ready);
+ when(templatePoolRef.getLocalDownloadPath()).thenReturn(null);
+
+ assertEquals(5368709120L, driver.getBytesRequiredForTemplate(templateInfo, storagePool));
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_NfsCached_ReturnsZero() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ when(storagePool.getId()).thenReturn(1L);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getState()).thenReturn(ObjectInDataStoreStateMachine.State.Ready);
+ when(templatePoolRef.getInstallPath()).thenReturn("/mnt/pool/template-uuid");
+
+ assertEquals(0L, driver.getBytesRequiredForTemplate(templateInfo, storagePool));
+ }
+
+ @Test
+ void testCreateAsync_TemplateWithISCSI_CreatesLunAndRecordsCloneSource() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getId()).thenReturn(7L);
+
+ Lun templateLun = new Lun();
+ templateLun.setName("/vol/vol1/cs_tmpl_50");
+ templateLun.setUuid("template-lun-uuid");
+ CloudStackVolume created = new CloudStackVolume();
+ created.setLun(templateLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.createTemplateCache(any(), any(), any(), anyLong())).thenReturn(created);
+
+ driver.createAsync(dataStore, templateInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ // local_download_path carries the clone source; install_path is left for grantAccess
+ verify(templatePoolRef).setLocalDownloadPath("template-lun-uuid");
+ verify(templatePoolRef).setTemplateSize(5368709120L);
+ verify(templatePoolRef, never()).setInstallPath(any());
+ verify(vmTemplatePoolDao).update(eq(7L), any(VMTemplateStoragePoolVO.class));
+ }
+ }
+
+
+ @Test
+ void testCreateAsync_TemplateWithISCSI_RollsBackLunWhenDbUpdateFails() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getId()).thenReturn(7L);
+ doThrow(new RuntimeException("db update failed")).when(vmTemplatePoolDao)
+ .update(eq(7L), any(VMTemplateStoragePoolVO.class));
+
+ Lun templateLun = new Lun();
+ templateLun.setName("/vol/vol1/cs_tmpl_50");
+ templateLun.setUuid("template-lun-uuid");
+ CloudStackVolume created = new CloudStackVolume();
+ created.setLun(templateLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.createTemplateCache(any(), any(), any(), anyLong())).thenReturn(created);
+ doNothing().when(sanStrategy).deleteCloudStackVolume(any());
+
+ driver.createAsync(dataStore, templateInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor deleteCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).deleteCloudStackVolume(deleteCaptor.capture());
+ assertEquals("template-lun-uuid", deleteCaptor.getValue().getLun().getUuid());
+ assertEquals("/vol/vol1/cs_tmpl_50", deleteCaptor.getValue().getLun().getName());
+ }
+ }
+
+ @Test
+ void testCreateAsync_TemplateWithISCSI_UnknownSize_Fails() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getSize()).thenReturn(0L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.createTemplateCache(any(), any(), any(), anyLong()))
+ .thenThrow(new CloudRuntimeException("Unknown virtual size for template [50]"));
+
+ driver.createAsync(dataStore, templateInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ @Test
+ void testCreateAsync_TemplateWithNFS_IsMetadataOnly() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getUuid()).thenReturn("template-uuid");
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ when(nasStrategy.createTemplateCache(any(), any(), any(), anyLong())).thenReturn(null);
+
+ driver.createAsync(dataStore, templateInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(vmTemplatePoolDao, never()).update(any(Long.class), any(VMTemplateStoragePoolVO.class));
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplate_ClonesWithoutGrowing() {
+ stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ assertEquals("/vol/vol1/cs_tmpl_50", requestCaptor.getValue().getLun().getClone().getSource().getName());
+ assertEquals("template-lun-uuid", requestCaptor.getValue().getLun().getClone().getSource().getUuid());
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), eq("cloned-lun-uuid"), eq(false));
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplate_GrowsWhenOfferingIsLarger() {
+ stubVolumeCloneFromTemplate(5368709120L, 21474836480L);
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(sanStrategy).resizeCloudStackVolume(eq(cloned), eq(21474836480L));
+ }
+ }
+
+ /**
+ * Sets up a volume create that the orchestrator has marked as a clone of a cached template.
+ */
+ private void stubVolumeCloneFromTemplate(long templateSize, long volumeSize) {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ lenient().when(volumeInfo.getSize()).thenReturn(volumeSize);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+ lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ lenient().when(volumeVO.getId()).thenReturn(100L);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
+
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ lenient().when(templatePoolRef.getLocalDownloadPath()).thenReturn("template-lun-uuid");
+ lenient().when(templatePoolRef.getTemplateSize()).thenReturn(templateSize);
+ }
+
+ @Test
+ void testGrantAccess_Template_WritesInstallPathAndRefreshesStoreTarget() {
+ PrimaryDataStore primaryDataStore = mock(PrimaryDataStore.class);
+ Map dataStoreDetails = new HashMap<>();
+ dataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET, "stale-value");
+
+ when(primaryDataStore.getId()).thenReturn(1L);
+ when(primaryDataStore.getDetails()).thenReturn(dataStoreDetails);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePool.getScope()).thenReturn(ScopeType.CLUSTER);
+ when(storagePool.getPath()).thenReturn("iqn.1992-08.com.netapp:sn.123456");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ when(templatePoolRef.getId()).thenReturn(7L);
+
+ when(host.getName()).thenReturn("host1");
+ when(host.getUuid()).thenReturn("host-uuid-1");
+
+ AccessGroup existingAccessGroup = new AccessGroup();
+ Igroup existingIgroup = new Igroup();
+ existingIgroup.setName("igroup1");
+ existingAccessGroup.setIgroup(existingIgroup);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
+ utilityMock.when(() -> OntapStorageUtils.getIgroupName(anyString(), anyString())).thenReturn("igroup1");
+
+ when(sanStrategy.getAccessGroup(any())).thenReturn(existingAccessGroup);
+ when(sanStrategy.ensureLunMapped(eq("svm1"), eq("/vol/vol1/cs_tmpl_50"), eq("igroup1"))).thenReturn("3");
+
+ assertTrue(driver.grantAccess(templateInfo, host, primaryDataStore));
+
+ String expectedPath = "/iqn.1992-08.com.netapp:sn.123456/3";
+ verify(templatePoolRef).setInstallPath(expectedPath);
+ verify(vmTemplatePoolDao).update(eq(7L), any(VMTemplateStoragePoolVO.class));
+
+ ArgumentCaptor
*
* Returns a {@link CloudStackVolume} so the driver can map it to {@code CreateCmdResult}
- * and update {@code template_spool_ref}. SAN populates {@code lun}; NAS returns an empty
- * volume (no LUN / file yet).
+ * and update {@code template_spool_ref}. SAN populates {@code lun}. NAS does not pre-create
+ * an object on the array and returns {@code null}; the driver treats that as no LUN and
+ * records the template uuid as the create path.
*
* @param storagePool CloudStack primary storage pool (one FlexVolume)
* @param templateInfo template being cached
* @param details pool details (SVM, protocol, etc.)
* @param sizeInBytes virtual size for the cache object (required for SAN; ignored for NAS)
- * @return created cache identity, or an empty {@link CloudStackVolume} when nothing is
- * pre-created on the array
+ * @return SAN cache identity with {@code lun} set, or {@code null} when nothing is
+ * pre-created on the array (NAS)
*/
abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
Map details, long sizeInBytes);
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 cc1e76a8e320..15e2fa89a271 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
@@ -100,6 +100,8 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
/**
* NFS template cache: nothing is pre-created on the array. The KVM agent writes the qcow2
* into the mounted FlexVolume; the framework records {@code install_path} afterward.
+ *
+ * @return {@code null}. The driver records the template uuid as the create path.
*/
@Override
public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
@@ -150,8 +152,9 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
if (cloudstackVolume.getVolumeInfo() == null) {
throw new CloudRuntimeException("Failed to clone file, volume info is missing from the request");
}
+ final long poolId = parseDatastoreId(cloudstackVolume.getDatastoreId());
- Map details = storagePoolDetailsDao.listDetailsKeyPairs(Long.parseLong(cloudstackVolume.getDatastoreId()));
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(poolId);
String flexVolUuid = details.get(OntapStorageConstants.VOLUME_UUID);
String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
if (flexVolUuid == null || flexVolUuid.isEmpty()) {
@@ -448,6 +451,18 @@ private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String sv
return exportPolicy;
}
+ /**
+ * Parses the pool id carried on a file-clone request. A non-numeric value fails before any
+ * ONTAP call, with the stored value in the error.
+ */
+ private long parseDatastoreId(String datastoreId) {
+ try {
+ return Long.parseLong(datastoreId);
+ } catch (NumberFormatException e) {
+ throw new CloudRuntimeException("Failed to clone file, datastore id [" + datastoreId + "] is not a number", e);
+ }
+ }
+
private String updateCloudStackVolumeMetadata(String dataStoreId, DataObject volumeInfo) {
logger.info("updateCloudStackVolumeMetadata called with datastoreID: {} volumeInfo: {} ", dataStoreId, volumeInfo );
if (volumeInfo == null) {
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 b9e32b081e4d..548fbe73be96 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
@@ -157,6 +157,11 @@ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, St
try {
String uuid = lunUuid;
if (uuid == null || uuid.isEmpty()) {
+ if (svmName == null || svmName.trim().isEmpty() || lunName == null || lunName.trim().isEmpty()) {
+ logger.warn("bestEffortDeleteTemplateCacheLun: Cannot look up leftover template cache LUN; "
+ + "svm [{}] or lun name [{}] is missing", svmName, lunName);
+ return;
+ }
Map lookup = Map.of(
OntapStorageConstants.NAME, lunName,
OntapStorageConstants.SVM_DOT_NAME, svmName);
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 4bacf83b2051..91f3bd27d0a3 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
@@ -933,6 +933,30 @@ void testCreateAsync_VolumeClonedFromTemplate_FailsWhenTemplateSizeIsInvalid() {
}
}
+ @Test
+ void testCreateAsync_Volume_InvalidCloneOfTemplateDetail() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "not-a-number", false));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("not-a-number"));
+ assertTrue(resultCaptor.getValue().getResult().contains("100"));
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ verify(nasStrategy, never()).cloneCloudStackVolume(any());
+ }
+
/**
* Sets up a volume create that the orchestrator has marked as a clone of a cached template.
*/
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
index 33e9ec3be2c0..38b7264b1f9a 100755
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
@@ -732,6 +732,23 @@ public void testCloneCloudStackVolume_MissingFlexVolUuid_Throws() {
verify(nasFeignClient, never()).cloneFile(anyString(), any(FileCloneRequest.class));
}
+ @Test
+ public void testCloneCloudStackVolume_InvalidDatastoreId_ThrowsBeforeClone() {
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ FileInfo source = new FileInfo();
+ source.setPath("template-uuid");
+ CloudStackVolume request = new CloudStackVolume();
+ request.setDatastoreId("not-a-pool");
+ request.setVolumeInfo(volumeObject);
+ request.setFile(source);
+ request.setDestinationPath("volume-uuid");
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
+ () -> strategy.cloneCloudStackVolume(request));
+ assertTrue(ex.getMessage().contains("not-a-pool"));
+ verify(nasFeignClient, never()).cloneFile(anyString(), any(FileCloneRequest.class));
+ }
+
@Test
public void testCloneCloudStackVolume_MissingDatastoreId_Throws() {
FileInfo source = new FileInfo();
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 700b63d15575..8acf6b0f8276 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
@@ -323,6 +323,34 @@ void testCreateTemplateCache_CleanupFailureDoesNotMaskCreateError() {
}
}
+ @Test
+ void testCreateTemplateCache_CreateFails_MissingSvmName_SkipsLookup() {
+ org.apache.cloudstack.storage.datastore.db.StoragePoolVO storagePool =
+ mock(org.apache.cloudstack.storage.datastore.db.StoragePoolVO.class);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePool.getHypervisor()).thenReturn(com.cloud.hypervisor.Hypervisor.HypervisorType.KVM);
+
+ org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo templateInfo =
+ mock(org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo.class);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ FeignException createEx = mock(FeignException.class);
+ when(createEx.status()).thenReturn(500);
+ when(createEx.getMessage()).thenReturn("create failed");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, org.mockito.Mockito.CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
+ .thenReturn(authHeader);
+ when(sanFeignClient.createLun(eq(authHeader), eq(true), any(Lun.class))).thenThrow(createEx);
+
+ CloudRuntimeException thrown = assertThrows(CloudRuntimeException.class,
+ () -> unifiedSANStrategy.createTemplateCache(storagePool, templateInfo, new HashMap<>(), 5368709120L));
+ assertTrue(thrown.getMessage().contains("create failed") || thrown.getMessage().contains("Failed to create"));
+ verify(sanFeignClient, never()).getLunResponse(any(), anyMap());
+ verify(sanFeignClient, never()).deleteLun(any(), any(), anyMap());
+ }
+ }
+
@Test
void testCreateCloudStackVolume_NullRequest_ThrowsException() {
assertThrows(CloudRuntimeException.class,
From b1d64d593e95abc8d2ad2a3804209f1f8cc2ba2f Mon Sep 17 00:00:00 2001
From: Rajiv Jain
Date: Thu, 8 Oct 2026 11:45:55 +0530
Subject: [PATCH 4/9] CSTACKEX-306: create volumes and templates from
CloudStack snapshots
Stack the snapshot-clone paths onto the ONTAP template-cache branch so a
volume or template can be created from a CloudStack volume snapshot that
already lives on the same primary pool.
- Clone a data volume from a snapshot on the same FlexVolume (iSCSI LUN
clone or NFS file clone). A root snapshot still produces a data volume;
the bootable path is create-template-from-snapshot.
- Copy a snapshot into a temporary volume when creating a template, then
remove that temporary volume record after the copy.
- Grow the clone when the disk offering is larger than the snapshot, and
delete the orphaned clone if resize fails.
- Keep template-cache behavior for clone-from-template, including the
existing pool types (Iscsi and NetworkFilesystem) on this branch.
Co-authored-by: Cursor
---
.../storage/volume/VolumeServiceImpl.java | 9 +-
.../driver/OntapPrimaryDatastoreDriver.java | 589 +++++++++++++---
.../storage/feign/model/FileCloneRequest.java | 54 +-
.../storage/service/StorageStrategy.java | 9 +-
.../storage/service/UnifiedNASStrategy.java | 10 +-
.../storage/service/UnifiedSANStrategy.java | 5 +-
.../service/model/CloudStackVolume.java | 14 +
.../storage/utils/OntapStorageConstants.java | 33 +
.../storage/utils/OntapStorageUtils.java | 43 ++
.../OntapPrimaryDatastoreDriverTest.java | 663 ++++++++++++++++++
.../service/UnifiedNASStrategyTest.java | 37 +
.../service/UnifiedSANStrategyTest.java | 41 ++
.../storage/utils/OntapStorageUtilsTest.java | 24 +
13 files changed, 1437 insertions(+), 94 deletions(-)
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 69b40f64af19..bc414248c176 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
@@ -1003,7 +1003,6 @@ private TemplateInfo createManagedTemplateVolume(TemplateInfo srcTemplateInfo, P
} else {
errMesg = callback.result.getResult();
}
- templateOnPrimary.processEvent(Event.OperationFailed);
throw new CloudRuntimeException(String.format("Unable to create template %s on primary storage %s: %s", templateOnPrimary.getImage(), destPrimaryDataStore, errMesg));
}
@@ -1011,8 +1010,12 @@ private TemplateInfo createManagedTemplateVolume(TemplateInfo srcTemplateInfo, P
} catch (Throwable e) {
logger.debug("Failed to create template volume on storage", e);
- templateOnPrimary.processEvent(Event.OperationFailed);
- throw new CloudRuntimeException(e.getMessage());
+ try {
+ templateOnPrimary.processEvent(Event.OperationFailed);
+ } catch (Exception stateEx) {
+ logger.warn("Unable to mark template {} as failed on primary storage {}: {}", templateOnPrimary.getImage(), destPrimaryDataStore, stateEx.getMessage());
+ }
+ throw new CloudRuntimeException(e.getMessage(), e);
} finally {
_tmpltPoolDao.releaseFromLockTable(templatePoolRefId);
}
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 363ea8a49aaa..c06c750f2c32 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
@@ -26,6 +26,9 @@
import com.cloud.exception.InvalidParameterValueException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
+import com.cloud.hypervisor.Hypervisor.HypervisorType;
+import com.cloud.storage.DataStoreRole;
+import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
import com.cloud.storage.StoragePool;
import com.cloud.storage.Volume;
@@ -33,6 +36,7 @@
import com.cloud.storage.VolumeVO;
import com.cloud.storage.ScopeType;
import com.cloud.storage.VMTemplateStoragePoolVO;
+import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.SnapshotDetailsDao;
import com.cloud.storage.dao.SnapshotDetailsVO;
import com.cloud.storage.dao.VMTemplatePoolDao;
@@ -55,6 +59,7 @@
import org.apache.commons.lang3.StringUtils;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
+import org.apache.cloudstack.storage.command.CopyCmdAnswer;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
@@ -81,7 +86,9 @@
import javax.inject.Inject;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
+import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -98,6 +105,7 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
@Inject private VolumeDao volumeDao;
@Inject private VolumeDetailsDao volumeDetailsDao;
@Inject private SnapshotDetailsDao snapshotDetailsDao;
+ @Inject private SnapshotDao snapshotDao;
@Inject private VMTemplatePoolDao vmTemplatePoolDao;
@Override
@@ -109,6 +117,11 @@ public Map getCapabilities() {
// Enables the framework to cache a template on the FlexVolume once and serve every later
// deployment with an array-side clone instead of another copy from secondary storage.
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString());
+ // createTemplate(snapshotid) reads the snapshot from this pool instead of backing it up to secondary first.
+ mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_TEMPLATE_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
+ // Must be present and false: StorageSystemDataMotionStrategy then clones the snapshot into a
+ // temporary volume (see copyAsync) and copies that volume to secondary storage.
+ mapCapabilities.put(OntapStorageConstants.CAN_DIRECT_ATTACH_SNAPSHOT, Boolean.FALSE.toString());
return mapCapabilities;
}
@@ -162,35 +175,41 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
// 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: a clone of the cached template when the
- // orchestrator asked for one, otherwise a blank LUN (iSCSI) or qcow2 file (NFS).
+ /*
+ * Create-volume combinations on ONTAP primary (v1):
+ *
+ * 1) cloneOfSnapshot — StorageSystemDataMotionStrategy sets volume_details.cloneOfSnapshot
+ * when createVolume(snapshotid) targets a managed backend snapshot.
+ * - Same primary pool / FlexVol only (PRIMARY_POOL_ID must match dataStore).
+ * - DATA or ROOT snapshot → new attachable data volume (ROOT is never bootable
+ * via this path; bootable recovery is createTemplate(snapshotid) → deploy).
+ * - Backend: iSCSI → POST /api/storage/luns (clone.source in .snapshot/);
+ * NFS → POST /api/storage/file/clone with snapshot.name.
+ * - IOPS: MIN_IOPS/MAX_IOPS may be on snapshot_details; apply is TODO below.
+ *
+ * 2) cloneOfTemplate — deploy / create from cached template on this pool.
+ *
+ * 3) else — blank LUN (iSCSI) or qcow2 (NFS).
+ *
+ * Mutually exclusive from the motion/orchestrator layer; snapshot is checked first
+ * (SolidFire-style) so a create-from-snapshot never accidentally falls through to blank create.
+ */
+ Long cloneOfSnapshotId = getSnapshotIdForCloning(volInfo.getId());
Long cloneOfTemplateId = getTemplateIdForCloning(volInfo.getId());
- CloudStackVolume clonedCloudStackVolume = cloneOfTemplateId != null
- ? cloneCloudStackVolumeFromTemplate(storagePool, volInfo, details, cloneOfTemplateId)
- : createCloudStackVolume(storagePool, volInfo, details);
-
- volumeVO.setPoolType(storagePool.getPoolType());
- volumeVO.setPoolId(storagePool.getId());
-
- if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
- // createCloudStackVolume validates the Feign response (LUN name + uuid) before returning
- Lun createdLun = clonedCloudStackVolume.getLun();
- String lunName = createdLun.getName();
-
- // Persist LUN details for future operations (delete, grant/revoke access)
- volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_UUID, createdLun.getUuid(), false);
- volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_NAME, lunName, false);
- volumeVO.setFolder(createdLun.getUuid());
-
- logger.info("createAsync: Created LUN [{}] for volume [{}]. LUN mapping will occur during grantAccess() to per-host igroup.",
- lunName, volumeVO.getId());
- createCmdResult = new CreateCmdResult(lunName, new Answer(null, true, null));
- } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
- createCmdResult = new CreateCmdResult(volInfo.getUuid(), new Answer(null, true, null));
- logger.info("createAsync: Managed NFS volume [{}] with path [{}] associated with pool {}",
- volumeVO.getId(), volInfo.getUuid(), storagePool.getId());
+ CloudStackVolume clonedCloudStackVolume;
+ if (cloneOfSnapshotId != null) {
+ clonedCloudStackVolume = cloneCloudStackVolumeFromSnapshot(
+ storagePool, volInfo, details, cloneOfSnapshotId);
+ // TODO(CSTACKEX-306): apply persisted MIN_IOPS / MAX_IOPS from snapshot_details
+ // onto this CloudStack volume (and ONTAP QoS if applicable) after successful clone.
+ } else if (cloneOfTemplateId != null) {
+ clonedCloudStackVolume = cloneCloudStackVolumeFromTemplate(
+ storagePool, volInfo, details, cloneOfTemplateId);
+ } else {
+ clonedCloudStackVolume = createCloudStackVolume(storagePool, volInfo, details);
}
- volumeDao.update(volumeVO.getId(), volumeVO);
+
+ createCmdResult = recordCreatedVolume(storagePool, volInfo, volumeVO, details, clonedCloudStackVolume);
}
} else if (dataObject.getType() == DataObjectType.TEMPLATE) {
createCmdResult = createTemplateOnPrimary(storagePool, (TemplateInfo) dataObject, details);
@@ -212,6 +231,41 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
}
}
+ /**
+ * Records pool association, image format and protocol-specific identity (LUN name/uuid for iSCSI)
+ * of a volume that was just created or cloned on ONTAP, and returns the create result whose path
+ * the framework stores on the volume.
+ */
+ private CreateCmdResult recordCreatedVolume(StoragePoolVO storagePool, VolumeInfo volInfo, VolumeVO volumeVO,
+ Map details, CloudStackVolume createdCloudStackVolume) {
+ CreateCmdResult createCmdResult = null;
+ volumeVO.setPoolType(storagePool.getPoolType());
+ volumeVO.setPoolId(storagePool.getId());
+ volumeVO.setFormat(getImageFormat(storagePool));
+ logger.info("createAsync: Volume format set to [{}] for pool type [{}]", volumeVO.getFormat(), storagePool.getPoolType());
+
+ if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
+ // createCloudStackVolume validates the Feign response (LUN name + uuid) before returning
+ Lun createdLun = createdCloudStackVolume.getLun();
+ String lunName = createdLun.getName();
+
+ // Persist LUN details for future operations (delete, grant/revoke access)
+ volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_UUID, createdLun.getUuid(), false);
+ volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_NAME, lunName, false);
+ volumeVO.setFolder(createdLun.getUuid());
+
+ logger.info("createAsync: Created LUN [{}] for volume [{}]. LUN mapping will occur during grantAccess() to per-host igroup.",
+ lunName, volumeVO.getId());
+ createCmdResult = new CreateCmdResult(lunName, new Answer(null, true, null));
+ } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
+ createCmdResult = new CreateCmdResult(volInfo.getUuid(), new Answer(null, true, null));
+ logger.info("createAsync: Managed NFS volume [{}] with path [{}] associated with pool {}",
+ volumeVO.getId(), volInfo.getUuid(), storagePool.getId());
+ }
+ volumeDao.update(volumeVO.getId(), volumeVO);
+ return createCmdResult;
+ }
+
/**
* Creates a volume on the ONTAP backend.
*/
@@ -304,7 +358,8 @@ private CloudStackVolume cloneCloudStackVolumeFromTemplate(StoragePoolVO storage
if (requestedSize > templateSize) {
logger.info("cloneCloudStackVolumeFromTemplate: Growing clone of template [{}] from {} to {} bytes for volume [{}]",
templateId, templateSize, requestedSize, volumeInfo.getId());
- storageStrategy.resizeCloudStackVolume(cloned, requestedSize);
+ resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize,
+ "cloneCloudStackVolumeFromTemplate template [" + templateId + "] volume [" + volumeInfo.getId() + "]");
}
return cloned;
@@ -329,6 +384,130 @@ private Long getTemplateIdForCloning(long volumeId) {
}
}
+ /**
+ * Returns the CloudStack snapshot id to clone from when {@code volume_details.cloneOfSnapshot}
+ * is set, or null when this create is not a create-volume-from-snapshot.
+ *
+ * Set by {@code StorageSystemDataMotionStrategy.handleCreateManagedVolumeFromManagedSnapshot}
+ * for the duration of {@code createAsync} only (same pattern as {@link #getTemplateIdForCloning}).
+ */
+ private Long getSnapshotIdForCloning(long volumeId) {
+ VolumeDetailVO detail = volumeDetailsDao.findDetail(volumeId, OntapStorageConstants.CLONE_OF_SNAPSHOT);
+ if (detail == null || detail.getValue() == null || detail.getValue().isEmpty()) {
+ return null;
+ }
+ return Long.valueOf(detail.getValue());
+ }
+
+ /**
+ * Creates a new volume on this pool by cloning a file/LUN from a CloudStack volume snapshot
+ * that already lives on the same FlexVolume.
+ *
+ * Combinations (product + plugin v1):
+ *
+ * - Same pool only — {@code snapshot_details.PRIMARY_POOL_ID} must equal this
+ * {@code storagePool}. Creating the volume on a different pool is descoped; use migrate later if needed.
+ * - DATA snapshot → attachable data disk (disk offering usually inherited).
+ * - ROOT snapshot → still a data disk here (not bootable). Bootable path remains
+ * {@code createTemplate(snapshotid)} then deploy.
+ * - iSCSI — {@code POST /api/storage/luns} with
+ * {@code clone.source.name=/vol/<fv>/.snapshot/<snap>/<lun>}
+ * - NFS3 — {@code POST /api/storage/file/clone} with {@code snapshot.name}
+ * - Not this path — in-place revert ({@code revertSnapshot}); VM/instance snapshots;
+ * secondary-storage archive restore.
+ *
+ *
+ * Optional grow when the disk offering is larger than the snapshot size (same pattern as
+ * clone-from-template).
+ */
+ private CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storagePool, VolumeInfo volumeInfo,
+ Map details, long csSnapshotId) {
+ String snapshotName = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.ONTAP_SNAP_NAME);
+ String volumePath = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.VOLUME_PATH);
+ String primaryPoolId = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.PRIMARY_POOL_ID);
+ String snapProtocol = requireSnapshotDetail(csSnapshotId, OntapStorageConstants.PROTOCOL);
+
+ // Same-pool / same-protocol gate (v1). Fail before any ONTAP call.
+ if (!String.valueOf(storagePool.getId()).equals(primaryPoolId)) {
+ throw new CloudRuntimeException("Create volume from snapshot [" + csSnapshotId
+ + "] requires the snapshot's primary pool [" + primaryPoolId
+ + "]; requested pool is [" + storagePool.getId() + "] (creating a volume from a snapshot on a different pool is not supported)");
+ }
+ String poolProtocol = details.get(OntapStorageConstants.PROTOCOL);
+ if (poolProtocol == null || !poolProtocol.equalsIgnoreCase(snapProtocol)) {
+ throw new CloudRuntimeException("Create volume from snapshot [" + csSnapshotId
+ + "] protocol mismatch: snapshot=[" + snapProtocol + "], pool=[" + poolProtocol + "]");
+ }
+
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+
+ logger.info("cloneCloudStackVolumeFromSnapshot: Cloning from CS snapshot [{}] (ONTAP snap [{}], path [{}]) "
+ + "for volume [{}] on pool [{}] protocol [{}]",
+ csSnapshotId, snapshotName, volumePath, volumeInfo.getId(), storagePool.getId(), poolProtocol);
+
+ CloudStackVolume request = isIscsi(details)
+ ? createCloneLunFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName)
+ : createCloneFileFromSnapshotRequest(storagePool, details, volumeInfo, volumePath, snapshotName);
+ CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request);
+ if (cloned == null) {
+ throw new CloudRuntimeException("ONTAP returned nothing when cloning snapshot [" + csSnapshotId
+ + "] for volume [" + volumeInfo.getId() + "]");
+ }
+
+ long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool);
+ long snapshotSize = resolveSnapshotSizeBytes(csSnapshotId);
+ if (snapshotSize > 0 && requestedSize > snapshotSize) {
+ logger.info("cloneCloudStackVolumeFromSnapshot: Growing clone of snapshot [{}] from {} to {} bytes for volume [{}]",
+ csSnapshotId, snapshotSize, requestedSize, volumeInfo.getId());
+ resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize,
+ "cloneCloudStackVolumeFromSnapshot snapshot [" + csSnapshotId + "] volume [" + volumeInfo.getId() + "]");
+ }
+
+ return cloned;
+ }
+
+ /**
+ * Grows a just-created ONTAP clone to the requested size. If resize fails, deletes the clone
+ * so the CloudStack create-failure path does not leave an orphaned LUN/file on the array.
+ */
+ private void resizeClonedVolumeOrCleanup(StorageStrategy storageStrategy, CloudStackVolume cloned,
+ long requestedSize, String context) {
+ try {
+ storageStrategy.resizeCloudStackVolume(cloned, requestedSize);
+ } catch (RuntimeException resizeEx) {
+ logger.error("{}: Resize to {} bytes failed; deleting orphaned ONTAP clone: {}",
+ context, requestedSize, resizeEx.getMessage());
+ bestEffortDeleteOrphanedClone(storageStrategy, cloned);
+ throw resizeEx;
+ }
+ }
+
+ private void bestEffortDeleteOrphanedClone(StorageStrategy storageStrategy, CloudStackVolume cloned) {
+ try {
+ storageStrategy.deleteCloudStackVolume(cloned);
+ logger.info("Deleted orphaned ONTAP clone after resize failure");
+ } catch (Exception cleanupEx) {
+ logger.warn("Failed to delete orphaned ONTAP clone after resize failure: {}", cleanupEx.getMessage());
+ }
+ }
+
+ private String requireSnapshotDetail(long csSnapshotId, String key) {
+ String value = getSnapshotDetail(csSnapshotId, key);
+ if (value == null || value.isEmpty()) {
+ throw new CloudRuntimeException("Missing snapshot_details [" + key + "] for snapshot [" + csSnapshotId
+ + "]; cannot create volume from snapshot");
+ }
+ return value;
+ }
+
+ private long resolveSnapshotSizeBytes(long csSnapshotId) {
+ SnapshotVO snapshotVO = snapshotDao.findById(csSnapshotId);
+ if (snapshotVO == null || snapshotVO.getSize() <= 0) {
+ return 0L;
+ }
+ return snapshotVO.getSize();
+ }
+
private VMTemplateStoragePoolVO findTemplatePoolRef(long poolId, long templateId) {
VMTemplateStoragePoolVO templatePoolRef = vmTemplatePoolDao.findByPoolTemplate(poolId, templateId, null);
if (templatePoolRef == null) {
@@ -469,6 +648,7 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac
CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool, details, volumeInfo);
storageStrategy.deleteCloudStackVolume(cloudStackVolumeRequest);
logger.info("deleteAsync: Volume deleted: " + volumeInfo.getId());
+ removeTemporarySnapshotCopyRecord(volumeInfo.getId());
commandResult.setResult(null);
commandResult.setSuccess(true);
} else if (data.getType() == DataObjectType.TEMPLATE) {
@@ -486,7 +666,10 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac
commandResult.setSuccess(false);
commandResult.setResult(e.getMessage());
} finally {
- callback.complete(commandResult);
+ // StorageSystemDataMotionStrategy deletes its temporary snapshot copy with a null callback.
+ if (callback != null) {
+ callback.complete(commandResult);
+ }
}
}
@@ -568,17 +751,143 @@ private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult comman
@Override
public void copyAsync(DataObject srcData, DataObject destData, AsyncCompletionCallback callback) {
- throw new UnsupportedOperationException("Copy operation is not supported for ONTAP primary storage.");
+ copyAsync(srcData, destData, null, callback);
}
+ /**
+ * Dispatches a driver-level copy by (source type, destination type). Only the pairs accepted by
+ * {@link #canCopy} are handled; everything else is left to the data motion strategies.
+ *
+ * Callers may pass a null callback (for example {@code StorageSystemDataMotionStrategy} for the
+ * temporary snapshot copy), in which case a failed copy is thrown.
+ */
@Override
public void copyAsync(DataObject srcData, DataObject destData, Host destHost, AsyncCompletionCallback callback) {
- throw new UnsupportedOperationException("Copy operation is not supported for ONTAP primary storage.");
+ CopyCommandResult result;
+ if (isSnapshotToTemporaryVolumeCopy(srcData, destData)) {
+ result = copySnapshotToTemporaryVolume(srcData, (VolumeInfo) destData);
+ } else {
+ throw new UnsupportedOperationException("Copy from " + describeForCopy(srcData) + " to "
+ + describeForCopy(destData) + " is not supported for ONTAP primary storage.");
+ }
+
+ if (callback != null) {
+ callback.complete(result);
+ } else if (!result.isSuccess()) {
+ throw new CloudRuntimeException("Failed to copy " + srcData.getType() + " [" + srcData.getId() + "] to "
+ + destData.getType() + " [" + destData.getId() + "]: " + result.getResult());
+ }
}
+ /**
+ * Returns true only for the (source type, destination type) pairs the driver copies itself.
+ *
+ * {@code DataMotionServiceImpl} consults {@code canCopy} before choosing a data motion strategy,
+ * so any pair accepted here bypasses {@code StorageSystemDataMotionStrategy} (volume migration,
+ * createVolume(snapshotid), template caching). Add a pair only together with its handler in
+ * {@link #copyAsync}.
+ */
@Override
public boolean canCopy(DataObject srcData, DataObject destData) {
- return false;
+ if (srcData == null || destData == null || srcData.getDataStore() == null || destData.getDataStore() == null) {
+ return false;
+ }
+ return isSnapshotToTemporaryVolumeCopy(srcData, destData);
+ }
+
+ private String describeForCopy(DataObject data) {
+ return data == null ? "null" : data.getType() + " [" + data.getId() + "]";
+ }
+
+ /**
+ * Matches the temporary volume {@code StorageSystemDataMotionStrategy} creates on the snapshot's own
+ * pool while copying the snapshot to secondary storage for {@code createTemplate(snapshotid)}.
+ */
+ private boolean isSnapshotToTemporaryVolumeCopy(DataObject srcData, DataObject destData) {
+ if (srcData == null || destData == null || srcData.getDataStore() == null || destData.getDataStore() == null) {
+ return false;
+ }
+ if (srcData.getType() != DataObjectType.SNAPSHOT || destData.getType() != DataObjectType.VOLUME) {
+ return false;
+ }
+ if (srcData.getDataStore().getRole() != DataStoreRole.Primary
+ || srcData.getDataStore().getId() != destData.getDataStore().getId()) {
+ return false;
+ }
+ return isTemporarySnapshotCopyVolume(volumeDao.findById(destData.getId()));
+ }
+
+ /**
+ * Clones a CloudStack volume snapshot into the temporary volume used for
+ * {@code createTemplate(snapshotid)}. The strategy then maps that volume to a KVM host, copies it
+ * into the template and deletes it again through {@link #deleteAsync}.
+ */
+ private CopyCommandResult copySnapshotToTemporaryVolume(DataObject srcData, VolumeInfo volInfo) {
+ try {
+ StoragePoolVO storagePool = storagePoolDao.findById(volInfo.getDataStore().getId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage Pool not found for id: " + volInfo.getDataStore().getId());
+ }
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ validateProtocol(details, volInfo.getDataStore());
+ VolumeVO volumeVO = volumeDao.findById(volInfo.getId());
+
+ renameTemporarySnapshotCopy(volInfo, volumeVO, srcData.getId());
+ logger.info("copySnapshotToTemporaryVolume: Cloning CS snapshot [{}] into temporary volume [{}] on pool [{}] for template creation",
+ srcData.getId(), volInfo.getId(), storagePool.getId());
+ CloudStackVolume cloned = cloneCloudStackVolumeFromSnapshot(storagePool, volInfo, details, srcData.getId());
+ if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
+ volumeVO.setPath(volInfo.getUuid());
+ }
+ recordCreatedVolume(storagePool, volInfo, volumeVO, details, cloned);
+ return new CopyCommandResult(null, new CopyCmdAnswer(volInfo.getTO()));
+ } catch (Exception e) {
+ logger.error("copySnapshotToTemporaryVolume: Failed to clone snapshot [{}] into volume [{}]: {}",
+ srcData.getId(), volInfo.getId(), e.getMessage());
+ CopyCommandResult result = new CopyCommandResult(null, new CopyCmdAnswer(e.getMessage()));
+ result.setResult(e.getMessage());
+ return result;
+ }
+ }
+
+ /**
+ * The temporary volume created by {@code StorageSystemDataMotionStrategy} for a snapshot copy is
+ * persisted in Allocated state without a disk offering; user volumes always carry a disk offering.
+ */
+ private boolean isTemporarySnapshotCopyVolume(VolumeVO volumeVO) {
+ if (volumeVO == null || volumeVO.getState() != Volume.State.Allocated) {
+ return false;
+ }
+ Long diskOfferingId = volumeVO.getDiskOfferingId();
+ return diskOfferingId == null || diskOfferingId == 0L;
+ }
+
+ /**
+ * The framework names the temporary volume {@code _.TMP}, which is not a valid
+ * ONTAP LUN name, so it is renamed before cloning. The in-memory object is updated as well because the
+ * SAN clone derives the LUN name from {@link VolumeInfo#getName()}.
+ */
+ private void renameTemporarySnapshotCopy(VolumeInfo volInfo, VolumeVO volumeVO, long csSnapshotId) {
+ String name = OntapStorageConstants.TEMP_SNAPSHOT_COPY_NAME_PREFIX + csSnapshotId + OntapStorageConstants.UNDERSCORE + volInfo.getId();
+ volumeVO.setName(name);
+ if (volInfo.getVolume() instanceof VolumeVO) {
+ ((VolumeVO) volInfo.getVolume()).setName(name);
+ }
+ }
+
+ /**
+ * {@code StorageSystemDataMotionStrategy} deletes the temporary snapshot copy on the array but leaves its
+ * volume row behind on success, so it is removed here once the backend object is gone.
+ */
+ private void removeTemporarySnapshotCopyRecord(long volumeId) {
+ VolumeVO volumeVO = volumeDao.findById(volumeId);
+ if (!isTemporarySnapshotCopyVolume(volumeVO)
+ || volumeVO.getName() == null || !volumeVO.getName().startsWith(OntapStorageConstants.TEMP_SNAPSHOT_COPY_NAME_PREFIX)) {
+ return;
+ }
+ volumeDetailsDao.removeDetails(volumeId);
+ volumeDao.remove(volumeId);
+ logger.info("deleteAsync: Removed temporary snapshot copy volume record [{}]", volumeId);
}
@Override
@@ -638,6 +947,7 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
volumeVO.setPoolType(storagePool.getPoolType());
volumeVO.setPoolId(storagePool.getId());
volumeDao.update(volumeVO.getId(), volumeVO);
+ syncTemporarySnapshotCopyPath(dataObject, volumeVO);
} else if (dataObject.getType() == DataObjectType.TEMPLATE) {
grantAccessTemplate((TemplateInfo) dataObject, host, dataStore, storagePool);
} else {
@@ -651,6 +961,22 @@ public boolean grantAccess(DataObject dataObject, Host host, DataStore dataStore
}
}
+ /**
+ * For the temporary snapshot copy, {@code StorageSystemDataMotionStrategy} builds the CopyCommand from
+ * the same in-memory volume object it passed to grantAccess, so the iSCSI path resolved by the LUN
+ * mapping must be reflected on that object too.
+ */
+ private void syncTemporarySnapshotCopyPath(DataObject dataObject, VolumeVO volumeVO) {
+ if (!isTemporarySnapshotCopyVolume(volumeVO) || !(dataObject instanceof VolumeInfo)) {
+ return;
+ }
+ Volume inMemoryVolume = ((VolumeInfo) dataObject).getVolume();
+ if (inMemoryVolume instanceof VolumeVO) {
+ ((VolumeVO) inMemoryVolume).setPath(volumeVO.getPath());
+ ((VolumeVO) inMemoryVolume).set_iScsiName(volumeVO.get_iScsiName());
+ }
+ }
+
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);
@@ -1074,10 +1400,10 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback snapshotDetails = new LinkedHashMap<>();
+ snapshotDetails.put(OntapStorageConstants.SRC_CS_VOLUME_ID, String.valueOf(volumeInfo.getId()));
+ snapshotDetails.put(OntapStorageConstants.BASE_ONTAP_FV_ID, flexVolUuid);
+ snapshotDetails.put(OntapStorageConstants.ONTAP_SNAP_ID, ontapSnapshotUuid);
+ snapshotDetails.put(OntapStorageConstants.ONTAP_SNAP_NAME, snapshotName);
+ snapshotDetails.put(OntapStorageConstants.VOLUME_PATH, volumePath);
+ snapshotDetails.put(OntapStorageConstants.PRIMARY_POOL_ID, String.valueOf(volumeVO.getPoolId()));
+ snapshotDetails.put(OntapStorageConstants.PROTOCOL, protocol);
+ // iSCSI only — needed for LUN restore / identity; null (skipped) for NFS.
+ snapshotDetails.put(OntapStorageConstants.LUN_DOT_UUID, lunUuid);
+ // TODO(CSTACKEX-306): on create-volume-from-snapshot, read these and apply to the new volume.
+ putPositiveIopsDetail(snapshotDetails, OntapStorageConstants.MIN_IOPS, volumeVO.getMinIops());
+ putPositiveIopsDetail(snapshotDetails, OntapStorageConstants.MAX_IOPS, volumeVO.getMaxIops());
+ updateSnapshotDetails(snapshot.getId(), snapshotDetails);
CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo);
result = new CreateCmdResult(null, createObjectAnswer);
@@ -1474,6 +1814,65 @@ private CloudStackVolume createCloneFileRequest(StoragePoolVO storagePool, Volum
return request;
}
+ /**
+ * Builds the request that clones a LUN out of a FlexVolume snapshot into a new volume LUN.
+ *
+ * {@code clone.source.name} is {@code /vol//.snapshot//}; ONTAP cannot
+ * resolve a snapshot-resident LUN by uuid, so no uuid is sent.
+ */
+ private CloudStackVolume createCloneLunFromSnapshotRequest(StoragePoolVO storagePool, Map details,
+ VolumeInfo volumeInfo, String sourceVolumePath,
+ String snapshotName) {
+ String lunName = volumeInfo.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
+ if (!OntapStorageUtils.isValidName(lunName)) {
+ throw new CloudRuntimeException("Invalid dataObject name [" + lunName
+ + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
+ }
+
+ String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
+ if (flexVolName == null || flexVolName.isEmpty()) {
+ flexVolName = storagePool.getName();
+ }
+
+ Svm svm = new Svm();
+ svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+
+ Lun.Source source = new Lun.Source();
+ source.setName(OntapStorageUtils.toLunCloneSourcePathInSnapshot(sourceVolumePath, flexVolName, snapshotName));
+ Lun.Clone clone = new Lun.Clone();
+ clone.setSource(source);
+
+ Lun lunRequest = new Lun();
+ lunRequest.setSvm(svm);
+ lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
+ lunRequest.setClone(clone);
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lunRequest);
+ return request;
+ }
+
+ /**
+ * Builds the request that clones a file out of a FlexVolume snapshot into a new file named after
+ * the volume uuid, inside the same FlexVolume.
+ */
+ private CloudStackVolume createCloneFileFromSnapshotRequest(StoragePoolVO storagePool, Map details,
+ VolumeInfo volumeInfo, String sourceVolumePath,
+ String snapshotName) {
+ String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
+
+ FileInfo file = new FileInfo();
+ file.setPath(OntapStorageUtils.toFlexVolRelativePath(sourceVolumePath, flexVolName));
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setDatastoreId(String.valueOf(storagePool.getId()));
+ request.setVolumeInfo(volumeInfo);
+ request.setFile(file);
+ request.setDestinationPath(OntapStorageUtils.toFlexVolRelativePath(volumeInfo.getUuid(), flexVolName));
+ request.setSnapshotName(snapshotName);
+ return request;
+ }
+
// ──────────────────────────────────────────────────────────────────────────
// Snapshot Helper Methods
// ──────────────────────────────────────────────────────────────────────────
@@ -1494,55 +1893,77 @@ private String buildSnapshotName(String volumeName, String snapshotUuid) {
}
/**
- * Persists snapshot metadata in snapshot_details table.
+ * Persists ONTAP snapshot metadata in {@code snapshot_details} for revert, delete, and
+ * create-volume-from-snapshot.
+ *
+ * Volume-snapshot delete reads {@code base_ontap_fv_id} and {@code ontap_snap_id} here
+ * during {@link #deleteOntapSnapshot}; missing rows prevent ONTAP cleanup.
*
- * @param csSnapshotId CloudStack snapshot ID
- * @param csVolumeId Source CloudStack volume ID
- * @param flexVolUuid ONTAP FlexVolume UUID
- * @param ontapSnapshotUuid ONTAP FlexVolume snapshot UUID
- * @param snapshotName ONTAP snapshot name
- * @param volumePath Path of the volume file/LUN within the FlexVolume (for restore)
- * @param storagePoolId Primary storage pool ID
- * @param protocol Storage protocol (NFS3 or ISCSI)
- * @param lunUuid LUN UUID (only for iSCSI, null for NFS)
+ * All rows go through {@link #persistSnapshotDetail} so DAO writes stay consistent;
+ * optional fields (LUN uuid, IOPS) are skipped when unset.
+ *
+ * @param csSnapshotId CloudStack snapshot ID
+ * @param details {@code snapshot_details} name to value, keyed by {@link OntapStorageConstants};
+ * entries with null or blank values are skipped
*/
- private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String flexVolUuid,
- String ontapSnapshotUuid, String snapshotName,
- String volumePath, long storagePoolId, String protocol,
- String lunUuid) {
- SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.SRC_CS_VOLUME_ID, String.valueOf(csVolumeId), false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.BASE_ONTAP_FV_ID, flexVolUuid, false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.ONTAP_SNAP_ID, ontapSnapshotUuid, false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.ONTAP_SNAP_NAME, snapshotName, false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.VOLUME_PATH, volumePath, false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.PRIMARY_POOL_ID, String.valueOf(storagePoolId), false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.PROTOCOL, protocol, false);
- snapshotDetailsDao.persist(snapshotDetail);
-
- // Store LUN UUID for iSCSI volumes (required for LUN restore API)
- if (lunUuid != null && !lunUuid.isEmpty()) {
- snapshotDetail = new SnapshotDetailsVO(csSnapshotId,
- OntapStorageConstants.LUN_DOT_UUID, lunUuid, false);
- snapshotDetailsDao.persist(snapshotDetail);
+ private void updateSnapshotDetails(long csSnapshotId, Map details) {
+ for (Map.Entry detail : details.entrySet()) {
+ persistSnapshotDetail(csSnapshotId, detail.getKey(), detail.getValue());
+ }
+ }
+
+ /**
+ * Adds an IOPS entry to {@code details} only when {@code iops} is non-null and positive.
+ */
+ private static void putPositiveIopsDetail(Map details, String detailName, Long iops) {
+ if (iops != null && iops > 0) {
+ details.put(detailName, String.valueOf(iops));
+ }
+ }
+
+ /**
+ * Persists one {@code snapshot_details} row. No-op when {@code value} is null or blank so
+ * optional keys (e.g. LUN uuid on NFS) share the same DAO path as required keys.
+ */
+ private void persistSnapshotDetail(long csSnapshotId, String detailName, String value) {
+ if (value == null || value.isEmpty()) {
+ return;
+ }
+ snapshotDetailsDao.persist(new SnapshotDetailsVO(csSnapshotId, detailName, value, false));
+ }
+
+ /**
+ * Only ISCSI and NFS3 pools can be provisioned. Any other protocol would fall through
+ * create and copy without producing a backend object.
+ */
+ private void validateProtocol(Map details, DataStore dataStore) {
+ String protocol = details == null ? null : details.get(OntapStorageConstants.PROTOCOL);
+ boolean supported = protocol != null && Arrays.stream(ProtocolType.values())
+ .anyMatch(type -> type.name().equalsIgnoreCase(protocol));
+ if (!supported) {
+ throw new CloudRuntimeException("Unsupported protocol [" + protocol + "] on storage pool ["
+ + dataStore.getName() + "]; supported protocols are " + Arrays.toString(ProtocolType.values()));
+ }
+ }
+
+ /**
+ * Image format stored on the CloudStack volume. This branch's iSCSI pools use
+ * {@link Storage.StoragePoolType#Iscsi}; NFS pools use {@link Storage.StoragePoolType#NetworkFilesystem}.
+ */
+ private Storage.ImageFormat getImageFormat(StoragePoolVO storagePool) {
+ HypervisorType hypervisorType = storagePool.getHypervisor();
+ if (!HypervisorType.KVM.equals(hypervisorType)) {
+ throw new CloudRuntimeException("Unsupported hypervisor [" + hypervisorType + "] for ONTAP image format resolution");
+ }
+ Storage.StoragePoolType spType = storagePool.getPoolType();
+ switch (spType) {
+ case Iscsi:
+ case IscsiLUN:
+ return Storage.ImageFormat.RAW;
+ case NetworkFilesystem:
+ return Storage.ImageFormat.QCOW2;
+ default:
+ throw new CloudRuntimeException("Unsupported pool type [" + spType + "] for ONTAP image format resolution");
}
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
index a9f2a106e9a8..cefff9fd282f 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
@@ -46,6 +46,15 @@ public class FileCloneRequest {
@JsonProperty("overwrite_destination")
private Boolean overwriteDestination;
+ /**
+ * Optional FlexVolume snapshot to clone from. When set, ONTAP clones {@code source_path}
+ * as it existed in that snapshot rather than from the live file/LUN.
+ *
+ * Used by create-volume-from-snapshot (same FlexVol). Omitted for live template-cache clones.
+ */
+ @JsonProperty("snapshot")
+ private SnapshotRef snapshot;
+
public FileCloneRequest() {
}
@@ -55,6 +64,14 @@ public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePat
this.destinationPath = destinationPath;
}
+ public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePath, String destinationPath,
+ String snapshotName) {
+ this(flexVolUuid, flexVolName, sourcePath, destinationPath);
+ if (snapshotName != null && !snapshotName.isEmpty()) {
+ this.snapshot = new SnapshotRef(snapshotName);
+ }
+ }
+
public VolumeRef getVolume() {
return volume;
}
@@ -87,6 +104,14 @@ public void setOverwriteDestination(Boolean overwriteDestination) {
this.overwriteDestination = overwriteDestination;
}
+ public SnapshotRef getSnapshot() {
+ return snapshot;
+ }
+
+ public void setSnapshot(SnapshotRef snapshot) {
+ this.snapshot = snapshot;
+ }
+
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class VolumeRef {
@@ -122,10 +147,37 @@ public void setName(String name) {
}
}
+ /**
+ * Snapshot identity for {@code POST /api/storage/file/clone} when cloning from a FlexVol snapshot.
+ */
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class SnapshotRef {
+
+ @JsonProperty("name")
+ private String name;
+
+ public SnapshotRef() {
+ }
+
+ public SnapshotRef(String name) {
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+
@Override
public String toString() {
return "FileCloneRequest{volume=" + (volume != null ? volume.getUuid() : null)
+ ", sourcePath=" + sourcePath
- + ", destinationPath=" + destinationPath + "}";
+ + ", destinationPath=" + destinationPath
+ + ", snapshot=" + (snapshot != null ? snapshot.getName() : null) + "}";
}
}
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 4d5baa2497ca..19dcb8e589bd 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
@@ -56,7 +56,6 @@
import java.util.Objects;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
-
/**
* Storage Strategy represents the communication path for all the ONTAP storage options
*
@@ -553,6 +552,14 @@ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool,
* ONTAP requires the source and the destination to live in the same FlexVolume, which
* holds because a CloudStack primary storage pool maps one-to-one onto a FlexVolume.
*
+ * The source may be an active object or one inside a FlexVolume snapshot:
+ *
+ * - SAN — {@code lun.clone.source.name} set to
+ * {@code /vol/<fv>/.snapshot/<snap>/<lun>}
+ * - NAS — {@link CloudStackVolume#getSnapshotName()} set; it is sent as
+ * {@code snapshot.name} on {@code POST /api/storage/file/clone}
+ *
+ *
* @param cloudstackVolume describes the clone to create; the source is carried in the
* protocol-specific clone reference (for SAN, {@code lun.clone.source})
* @return the created CloudStackVolume, populated with the backend identity of the clone
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 15e2fa89a271..82300df6dbc7 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
@@ -137,7 +137,8 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
* Clones a file inside the FlexVolume using ONTAP's file clone API.
*
* The source is taken from {@code file.path} and the destination from
- * {@code destinationPath}, both relative to the root of the FlexVolume backing the pool.
+ * {@code destinationPath}, both relative to the root of the FlexVolume backing the pool.
+ * When {@code snapshotName} is set, the source is read from that FlexVolume snapshot.
*/
@Override
public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume) {
@@ -162,10 +163,12 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
}
String sourcePath = cloudstackVolume.getFile().getPath();
String destinationPath = cloudstackVolume.getDestinationPath();
+ String snapshotName = cloudstackVolume.getSnapshotName();
- logger.info("cloneCloudStackVolume: Cloning file [{}] to [{}] in FlexVol [{}]", sourcePath, destinationPath, flexVolName);
+ logger.info("cloneCloudStackVolume: Cloning file [{}] to [{}] in FlexVol [{}] from snapshot [{}]",
+ sourcePath, destinationPath, flexVolName, snapshotName);
try {
- FileCloneRequest request = new FileCloneRequest(flexVolUuid, flexVolName, sourcePath, destinationPath);
+ FileCloneRequest request = new FileCloneRequest(flexVolUuid, flexVolName, sourcePath, destinationPath, snapshotName);
JobResponse jobResponse = nasFeignClient.cloneFile(getAuthHeader(), request);
pollJobIfPresent(jobResponse, "clone file [" + sourcePath + "] to [" + destinationPath + "]");
@@ -178,6 +181,7 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
clonedCloudStackVolume.setFile(clonedFile);
clonedCloudStackVolume.setDatastoreId(cloudstackVolume.getDatastoreId());
clonedCloudStackVolume.setVolumeInfo(cloudstackVolume.getVolumeInfo());
+ clonedCloudStackVolume.setSnapshotName(snapshotName);
return clonedCloudStackVolume;
} catch (FeignException e) {
logger.error("FeignException occurred while cloning file [{}], Status: {}, Exception: {}",
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 548fbe73be96..3341ba12e354 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
@@ -110,7 +110,9 @@ public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateI
Map details, long sizeInBytes) {
if (sizeInBytes <= 0) {
throw new CloudRuntimeException("Unknown virtual size for template [" + templateInfo.getId()
- + "]; cannot size the template LUN on pool [" + storagePool.getId() + "]");
+ + "]; cannot size the template LUN on pool [" + storagePool.getId() + "]. The template size in vm_template"
+ + " is unset; verify the template was registered/seeded with its virtual size (virtualsize in"
+ + " template.properties on secondary storage).");
}
CloudStackVolume request = buildTemplateLunRequest(storagePool, details, templateInfo.getId(), sizeInBytes);
@@ -265,7 +267,6 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
}
}
-
/**
* Ensures ONTAP returned a usable LUN identity from create/clone. Callers in the datastore
* driver rely on non-null name and uuid, so reject incomplete records at the Feign boundary.
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 ab38e3045f51..5147e9ff7ac2 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
@@ -51,6 +51,12 @@ public class CloudStackVolume {
*/
private String destinationPath;
+ /**
+ * ONTAP FlexVolume snapshot name when cloning a new file/LUN from a snapshot
+ * (create-volume-from-snapshot). Null for live clones (e.g. template cache).
+ */
+ private String snapshotName;
+
private DataObject volumeInfo; // This is needed as we need DataObject to be passed to agent to create volume
public FileInfo getFile() {
@@ -101,4 +107,12 @@ public void setDestinationPath(String destinationPath) {
this.destinationPath = destinationPath;
}
+ public String getSnapshotName() {
+ return snapshotName;
+ }
+
+ public void setSnapshotName(String snapshotName) {
+ this.snapshotName = snapshotName;
+ }
+
}
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 51205069ee2a..2dcf623fa947 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
@@ -83,6 +83,12 @@ public class OntapStorageConstants {
public static final String VOLUME_PATH_PREFIX = "/vol/";
+ /**
+ * Path segment inserted after the FlexVol name when identifying a LUN inside a FlexVol snapshot
+ * for {@code POST /api/storage/luns} clone ({@code /vol/<fv>/.snapshot/<snap>/<lun>}).
+ */
+ public static final String SNAPSHOT_PATH_SEGMENT = "/.snapshot/";
+
public static final String ONTAP_NAME_REGEX = "^[a-zA-Z][a-zA-Z0-9_]*$";
public static final String KVM = "KVM";
@@ -104,6 +110,13 @@ public class OntapStorageConstants {
public static final String VOLUME_PATH = "volume_path";
public static final String PRIMARY_POOL_ID = "primary_pool_id";
public static final String ONTAP_SNAP_SIZE = "ontap_snap_size";
+ /**
+ * Optional {@code snapshot_details} keys: min/max IOPS from the source volume at take-snapshot
+ * time. Persisted only when the volume has configured values; applied to volumes created from
+ * the snapshot in a later change (see TODO on create-from-snapshot).
+ */
+ public static final String MIN_IOPS = "min_iops";
+ public static final String MAX_IOPS = "max_iops";
public static final String FILE_PATH = "file_path";
public static final int MAX_SNAPSHOT_NAME_LENGTH = 255;
@@ -120,4 +133,24 @@ public class OntapStorageConstants {
* with the string used by the orchestrator.
*/
public static final String CLONE_OF_TEMPLATE = "cloneOfTemplate";
+
+ /**
+ * Key of the {@code volume_details} row that {@code StorageSystemDataMotionStrategy} writes
+ * immediately before {@code createAsync} when a volume is to be created from a CloudStack
+ * snapshot already present on this pool. The value is the CloudStack snapshot id. The literal
+ * must stay in sync with the string used by the orchestrator.
+ */
+ public static final String CLONE_OF_SNAPSHOT = "cloneOfSnapshot";
+
+ /**
+ * Driver capability read by {@code StorageSystemDataMotionStrategy} when copying a snapshot to
+ * secondary storage. The literal must stay in sync with the string used by the orchestrator.
+ */
+ public static final String CAN_DIRECT_ATTACH_SNAPSHOT = "CAN_DIRECT_ATTACH_SNAPSHOT";
+
+ /**
+ * Name prefix of the temporary volume a snapshot is cloned into for createTemplate(snapshotid),
+ * suffixed with the CloudStack snapshot id and volume id.
+ */
+ public static final String TEMP_SNAPSHOT_COPY_NAME_PREFIX = "cs_tmp_snap_";
}
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 6cc06d75c8ad..c58415bea914 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
@@ -19,6 +19,7 @@
package org.apache.cloudstack.storage.utils;
+import com.cloud.exception.InvalidParameterValueException;
import com.cloud.utils.StringUtils;
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.storage.feign.model.Lun;
@@ -111,4 +112,46 @@ public static String getLunName(String volName, String lunName) {
return OntapStorageConstants.VOLUME_PATH_PREFIX + volName + OntapStorageConstants.SLASH + lunName;
}
+ /**
+ * Converts a path stored in CloudStack (absolute LUN {@code /vol/<flexVol>/...} or already
+ * relative NFS file path) into the FlexVol-relative path expected by
+ * {@code POST /api/storage/file/clone}.
+ */
+ public static String toFlexVolRelativePath(String path, String flexVolName) {
+ if (path == null || path.isEmpty()) {
+ return path;
+ }
+ if (flexVolName != null && !flexVolName.isEmpty()) {
+ String prefix = OntapStorageConstants.VOLUME_PATH_PREFIX + flexVolName + OntapStorageConstants.SLASH;
+ if (path.startsWith(prefix)) {
+ return path.substring(prefix.length());
+ }
+ }
+ // Already relative (typical NFS uuid path) or unexpected absolute form — strip a leading slash.
+ return path.startsWith(OntapStorageConstants.SLASH) ? path.substring(1) : path;
+ }
+
+ /**
+ * Builds the ONTAP LUN clone source name that points at a LUN inside a FlexVol snapshot.
+ *
+ * Format required by {@code POST /api/storage/luns} when cloning from a snapshot:
+ * {@code /vol/<flexVol>/.snapshot/<snapshotName>/<relativeLunPath>}.
+ *
+ * {@code clone.source.uuid} cannot identify a snapshot-resident LUN; name must be used.
+ */
+ public static String toLunCloneSourcePathInSnapshot(String lunPath, String flexVolName, String snapshotName) {
+ if (flexVolName == null || flexVolName.isEmpty()) {
+ throw new InvalidParameterValueException("FlexVolume name is required to build a snapshot LUN path");
+ }
+ if (snapshotName == null || snapshotName.isEmpty()) {
+ throw new InvalidParameterValueException("Snapshot name is required to build a snapshot LUN path");
+ }
+ String relativeLunPath = toFlexVolRelativePath(lunPath, flexVolName);
+ if (relativeLunPath == null || relativeLunPath.isEmpty()) {
+ throw new InvalidParameterValueException("LUN path is required to build a snapshot LUN path");
+ }
+ return OntapStorageConstants.VOLUME_PATH_PREFIX + flexVolName
+ + OntapStorageConstants.SNAPSHOT_PATH_SEGMENT + snapshotName
+ + OntapStorageConstants.SLASH + relativeLunPath;
+ }
}
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 91f3bd27d0a3..3a8d21e3fd97 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
@@ -22,19 +22,27 @@
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
+import com.cloud.storage.DataStoreRole;
import com.cloud.storage.ScopeType;
+import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
import com.cloud.storage.VMTemplateStoragePoolVO;
+import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.SnapshotDao;
+import com.cloud.storage.dao.SnapshotDetailsDao;
+import com.cloud.storage.dao.SnapshotDetailsVO;
import com.cloud.storage.dao.VMTemplatePoolDao;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.exception.CloudRuntimeException;
+import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
+import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
@@ -42,6 +50,7 @@
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.FileInfo;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
@@ -63,8 +72,10 @@
import java.util.HashMap;
import java.util.Map;
+import static com.cloud.agent.api.to.DataObjectType.SNAPSHOT;
import static com.cloud.agent.api.to.DataObjectType.TEMPLATE;
import static com.cloud.agent.api.to.DataObjectType.VOLUME;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -137,6 +148,11 @@ class OntapPrimaryDatastoreDriverTest {
@Mock
private AsyncCompletionCallback commandCallback;
+ @Mock
+ private SnapshotDetailsDao snapshotDetailsDao;
+ @Mock
+ private SnapshotDao snapshotDao;
+
@InjectMocks
private OntapPrimaryDatastoreDriver driver;
@@ -159,6 +175,8 @@ void testGetCapabilities() {
assertEquals(Boolean.TRUE.toString(), capabilities.get("STORAGE_SYSTEM_SNAPSHOT"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_SNAPSHOT"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_VOLUME"));
+ assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_TEMPLATE_FROM_SNAPSHOT"));
+ assertEquals(Boolean.FALSE.toString(), capabilities.get(OntapStorageConstants.CAN_DIRECT_ATTACH_SNAPSHOT));
}
@Test
@@ -240,6 +258,7 @@ void testCreateAsync_VolumeWithNFS_Success() {
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
when(volumeVO.getId()).thenReturn(100L);
@@ -943,6 +962,7 @@ void testCreateAsync_Volume_InvalidCloneOfTemplateDetail() {
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "not-a-number", false));
@@ -957,6 +977,333 @@ void testCreateAsync_Volume_InvalidCloneOfTemplateDetail() {
verify(nasStrategy, never()).cloneCloudStackVolume(any());
}
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_IscsiSuccessWithoutGrow() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("snap-cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ Lun lunRequest = requestCaptor.getValue().getLun();
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun", lunRequest.getClone().getSource().getName());
+ assertNull(lunRequest.getClone().getSource().getUuid());
+ assertEquals("/vol/vol1/test_volume", lunRequest.getName());
+ assertEquals("svm1", lunRequest.getSvm().getName());
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), eq("snap-cloned-lun-uuid"), eq(false));
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_GrowsWhenOfferingIsLarger() {
+ stubVolumeCloneFromSnapshot(5368709120L, 21474836480L, ProtocolType.ISCSI.name());
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("snap-cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(sanStrategy).resizeCloudStackVolume(eq(cloned), eq(21474836480L));
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_ResizeFails_DeletesOrphanedClone() {
+ stubVolumeCloneFromSnapshot(5368709120L, 21474836480L, ProtocolType.ISCSI.name());
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("snap-cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+ doThrow(new CloudRuntimeException("resize failed"))
+ .when(sanStrategy).resizeCloudStackVolume(eq(cloned), eq(21474836480L));
+ doNothing().when(sanStrategy).deleteCloudStackVolume(eq(cloned));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).deleteCloudStackVolume(eq(cloned));
+ verify(volumeDetailsDao, never()).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), anyString(), anyBoolean());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_NfsSuccess() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.NFS3.name());
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(volumeInfo.getUuid()).thenReturn("new-volume-uuid");
+
+ CloudStackVolume cloned = new CloudStackVolume();
+ FileInfo file = new FileInfo();
+ file.setPath("new-volume-uuid");
+ cloned.setFile(file);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ when(nasStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(nasStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ CloudStackVolume request = requestCaptor.getValue();
+ assertEquals("source-file-uuid", request.getFile().getPath());
+ assertEquals("new-volume-uuid", request.getDestinationPath());
+ assertEquals("snap_cs200", request.getSnapshotName());
+ assertEquals("1", request.getDatastoreId());
+ verify(nasStrategy, never()).createCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_PoolMismatch_Fails() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+ when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PRIMARY_POOL_ID))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PRIMARY_POOL_ID, "999", false));
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_MissingDetail_Fails() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+ when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.ONTAP_SNAP_NAME)).thenReturn(null);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_ProtocolMismatch_Fails() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+ // Pool is iSCSI (default stub details) but snapshot was taken on NFS.
+ when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PROTOCOL))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name(), false));
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_NullStrategyResult_Fails() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(null);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_StrategyThrows_Fails() {
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenThrow(new CloudRuntimeException("ONTAP clone failed"));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_PrefersSnapshotOverTemplate() {
+ // Corner: when both details are present, the snapshot clone wins over the template clone.
+ stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("snap-cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun",
+ requestCaptor.getValue().getLun().getClone().getSource().getName());
+ verify(vmTemplatePoolDao, never()).findByPoolTemplate(anyLong(), anyLong(), any());
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_NfsGrowsWhenOfferingIsLarger() {
+ stubVolumeCloneFromSnapshot(5368709120L, 21474836480L, ProtocolType.NFS3.name());
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(volumeInfo.getUuid()).thenReturn("new-volume-uuid");
+
+ CloudStackVolume cloned = new CloudStackVolume();
+ FileInfo file = new FileInfo();
+ file.setPath("new-volume-uuid");
+ cloned.setFile(file);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ when(nasStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(nasStrategy).resizeCloudStackVolume(eq(cloned), eq(21474836480L));
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromSnapshot_SkipsGrowWhenSnapshotSizeUnknown() {
+ stubVolumeCloneFromSnapshot(0L, 21474836480L, ProtocolType.ISCSI.name());
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("snap-cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ /**
+ * Sets up a volume create that the orchestrator has marked as a clone of a CloudStack snapshot.
+ */
+ private void stubVolumeCloneFromSnapshot(long snapshotSize, long volumeSize, String protocol) {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ lenient().when(volumeInfo.getSize()).thenReturn(volumeSize);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ // getId is only used after snapshot_details validation succeeds; lenient for early-fail tests.
+ lenient().when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+ lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ lenient().when(volumeVO.getId()).thenReturn(100L);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT, "200", false));
+
+ String volumePath = ProtocolType.NFS3.name().equalsIgnoreCase(protocol)
+ ? "source-file-uuid"
+ : "/vol/vol1/source_lun";
+ if (!ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) {
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "vol1");
+ }
+ // Lenient so early-fail tests can override/null individual keys without STRICT_STUBS noise.
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.ONTAP_SNAP_NAME))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.ONTAP_SNAP_NAME, "snap_cs200", false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.VOLUME_PATH))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.VOLUME_PATH, volumePath, false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PRIMARY_POOL_ID))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PROTOCOL))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PROTOCOL, protocol, false));
+
+ SnapshotVO snapshotVO = mock(SnapshotVO.class);
+ lenient().when(snapshotDao.findById(200L)).thenReturn(snapshotVO);
+ lenient().when(snapshotVO.getSize()).thenReturn(snapshotSize);
+ }
+
/**
* Sets up a volume create that the orchestrator has marked as a clone of a cached template.
*/
@@ -977,6 +1324,7 @@ private void stubVolumeCloneFromTemplate(long templateSize, long volumeSize) {
when(volumeDao.findById(100L)).thenReturn(volumeVO);
lenient().when(volumeVO.getId()).thenReturn(100L);
+ lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
@@ -1409,6 +1757,7 @@ void testCreateAsync_VolumeClonedFromTemplate_MissingSpoolRef_Fails() {
when(storagePool.getId()).thenReturn(1L);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
@@ -1424,4 +1773,318 @@ void testCreateAsync_VolumeClonedFromTemplate_MissingSpoolRef_Fails() {
verify(sanStrategy, never()).cloneCloudStackVolume(any());
}
}
+ private static VolumeVO temporarySnapshotCopyVolume() {
+ return new VolumeVO(Volume.Type.DATADISK, "ROOT-5_20260924.TMP", 1L, 1L, 2L, 0L,
+ Storage.ProvisioningType.THIN, 5368709120L, 0L, 0L, "");
+ }
+
+ private SnapshotInfo stubSnapshotToVolumeCopy(VolumeVO destVolume) {
+ SnapshotInfo snapshotInfo = mock(SnapshotInfo.class);
+ lenient().when(snapshotInfo.getType()).thenReturn(SNAPSHOT);
+ lenient().when(snapshotInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(snapshotInfo.getId()).thenReturn(200L);
+ lenient().when(volumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(volumeInfo.getId()).thenReturn(100L);
+ // VolumeObject.getName() reads the VolumeVO, which copyAsync renames before cloning.
+ lenient().when(volumeInfo.getName()).thenReturn("cs_tmp_snap_200_100");
+ lenient().when(dataStore.getId()).thenReturn(1L);
+ lenient().when(dataStore.getRole()).thenReturn(DataStoreRole.Primary);
+ lenient().when(volumeDao.findById(100L)).thenReturn(destVolume);
+ return snapshotInfo;
+ }
+
+ private void stubSnapshotCloneSource(String protocol) {
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ lenient().when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+ lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, protocol);
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "vol1");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ lenient().when(volumeInfo.getSize()).thenReturn(5368709120L);
+
+ String volumePath = ProtocolType.NFS3.name().equals(protocol) ? "source-file-uuid" : "/vol/vol1/source_lun";
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.ONTAP_SNAP_NAME))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.ONTAP_SNAP_NAME, "snap_cs200", false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.VOLUME_PATH))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.VOLUME_PATH, volumePath, false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PRIMARY_POOL_ID))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false));
+ lenient().when(snapshotDetailsDao.findDetail(200L, OntapStorageConstants.PROTOCOL))
+ .thenReturn(new SnapshotDetailsVO(200L, OntapStorageConstants.PROTOCOL, protocol, false));
+ SnapshotVO snapshotVO = mock(SnapshotVO.class);
+ lenient().when(snapshotDao.findById(200L)).thenReturn(snapshotVO);
+ lenient().when(snapshotVO.getSize()).thenReturn(5368709120L);
+ }
+
+ @Test
+ void testCanCopy_TemporarySnapshotCopyOnSamePool_ReturnsTrue() {
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(temporarySnapshotCopyVolume());
+
+ assertTrue(driver.canCopy(snapshotInfo, volumeInfo));
+ }
+
+ @Test
+ void testCanCopy_CreateVolumeFromSnapshot_ReturnsFalse() {
+ VolumeVO userVolume = temporarySnapshotCopyVolume();
+ userVolume.setDiskOfferingId(5L);
+ userVolume.setState(Volume.State.Creating);
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(userVolume);
+
+ assertFalse(driver.canCopy(snapshotInfo, volumeInfo));
+ }
+
+ @Test
+ void testCanCopy_AllocatedVolumeWithDiskOffering_ReturnsFalse() {
+ VolumeVO userVolume = temporarySnapshotCopyVolume();
+ userVolume.setDiskOfferingId(5L);
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(userVolume);
+
+ assertFalse(driver.canCopy(snapshotInfo, volumeInfo));
+ }
+
+ @Test
+ void testCanCopy_DifferentPool_ReturnsFalse() {
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(temporarySnapshotCopyVolume());
+ DataStore otherStore = mock(DataStore.class);
+ when(otherStore.getId()).thenReturn(2L);
+ when(volumeInfo.getDataStore()).thenReturn(otherStore);
+
+ assertFalse(driver.canCopy(snapshotInfo, volumeInfo));
+ verify(volumeDao, never()).findById(anyLong());
+ }
+
+ @Test
+ void testCanCopy_TemplateCacheCopies_ReturnFalse() {
+ TemplateInfo templateOnPrimary = mock(TemplateInfo.class);
+ lenient().when(templateInfo.getType()).thenReturn(TEMPLATE);
+ lenient().when(templateInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(templateOnPrimary.getType()).thenReturn(TEMPLATE);
+ lenient().when(templateOnPrimary.getDataStore()).thenReturn(dataStore);
+ lenient().when(volumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(volumeInfo.getDataStore()).thenReturn(dataStore);
+
+ assertFalse(driver.canCopy(templateInfo, templateOnPrimary));
+ assertFalse(driver.canCopy(templateOnPrimary, volumeInfo));
+ verify(volumeDao, never()).findById(anyLong());
+ }
+
+ @Test
+ void testCanCopy_VolumeMigration_ReturnsFalse() {
+ VolumeInfo destVolumeInfo = mock(VolumeInfo.class);
+ DataStore destStore = mock(DataStore.class);
+ lenient().when(volumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(destVolumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(destVolumeInfo.getDataStore()).thenReturn(destStore);
+
+ assertFalse(driver.canCopy(volumeInfo, destVolumeInfo));
+ verify(volumeDao, never()).findById(anyLong());
+ }
+
+ @Test
+ void testCopyAsync_VolumeMigration_Throws() {
+ VolumeInfo destVolumeInfo = mock(VolumeInfo.class);
+ DataStore destStore = mock(DataStore.class);
+ lenient().when(volumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(volumeInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(destVolumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(destVolumeInfo.getDataStore()).thenReturn(destStore);
+
+ assertThrows(UnsupportedOperationException.class,
+ () -> driver.copyAsync(volumeInfo, destVolumeInfo, null, null));
+ verify(sanStrategy, never()).cloneCloudStackVolume(any());
+ verify(nasStrategy, never()).cloneCloudStackVolume(any());
+ }
+
+ @Test
+ void testCopyAsync_UnsupportedPair_Throws() {
+ lenient().when(templateInfo.getType()).thenReturn(TEMPLATE);
+ lenient().when(templateInfo.getDataStore()).thenReturn(dataStore);
+ lenient().when(volumeInfo.getType()).thenReturn(VOLUME);
+ lenient().when(volumeInfo.getDataStore()).thenReturn(dataStore);
+
+ assertThrows(UnsupportedOperationException.class, () -> driver.copyAsync(templateInfo, volumeInfo, null, null));
+ }
+
+ @Test
+ void testCopyAsync_IscsiTemporarySnapshotCopy_ClonesLunAndRecordsIdentity() {
+ VolumeVO dbVolume = temporarySnapshotCopyVolume();
+ VolumeVO inMemoryVolume = temporarySnapshotCopyVolume();
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(dbVolume);
+ when(volumeInfo.getVolume()).thenReturn(inMemoryVolume);
+ stubSnapshotCloneSource(ProtocolType.ISCSI.name());
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/cs_tmp_snap_200_100");
+ clonedLun.setUuid("tmp-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.copyAsync(snapshotInfo, volumeInfo, null, null);
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun",
+ requestCaptor.getValue().getLun().getClone().getSource().getName());
+ assertEquals("/vol/vol1/cs_tmp_snap_200_100", requestCaptor.getValue().getLun().getName());
+
+ assertEquals("cs_tmp_snap_200_100", dbVolume.getName());
+ assertEquals("cs_tmp_snap_200_100", inMemoryVolume.getName());
+ assertEquals(Storage.ImageFormat.RAW, dbVolume.getFormat());
+ assertEquals("tmp-lun-uuid", dbVolume.getFolder());
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), eq("tmp-lun-uuid"), eq(false));
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_NAME), eq("/vol/vol1/cs_tmp_snap_200_100"), eq(false));
+ verify(volumeDao).update(anyLong(), eq(dbVolume));
+ verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ }
+ }
+
+ @Test
+ void testCopyAsync_NfsTemporarySnapshotCopy_SetsFilePath() {
+ VolumeVO dbVolume = temporarySnapshotCopyVolume();
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(dbVolume);
+ when(volumeInfo.getVolume()).thenReturn(temporarySnapshotCopyVolume());
+ when(volumeInfo.getUuid()).thenReturn("tmp-volume-uuid");
+ stubSnapshotCloneSource(ProtocolType.NFS3.name());
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+
+ CloudStackVolume cloned = new CloudStackVolume();
+ FileInfo file = new FileInfo();
+ file.setPath("tmp-volume-uuid");
+ cloned.setFile(file);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ when(nasStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.copyAsync(snapshotInfo, volumeInfo, null, null);
+
+ ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(CloudStackVolume.class);
+ verify(nasStrategy).cloneCloudStackVolume(requestCaptor.capture());
+ assertEquals("source-file-uuid", requestCaptor.getValue().getFile().getPath());
+ assertEquals("tmp-volume-uuid", requestCaptor.getValue().getDestinationPath());
+ assertEquals("snap_cs200", requestCaptor.getValue().getSnapshotName());
+
+ assertEquals("tmp-volume-uuid", dbVolume.getPath());
+ assertEquals(Storage.ImageFormat.QCOW2, dbVolume.getFormat());
+ verify(volumeDao).update(anyLong(), eq(dbVolume));
+ }
+ }
+
+ @Test
+ void testCopyAsync_CloneFailsWithoutCallback_Throws() {
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(temporarySnapshotCopyVolume());
+ when(volumeInfo.getVolume()).thenReturn(temporarySnapshotCopyVolume());
+ stubSnapshotCloneSource(ProtocolType.ISCSI.name());
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenThrow(new CloudRuntimeException("clone failed"));
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
+ () -> driver.copyAsync(snapshotInfo, volumeInfo, null, null));
+ assertTrue(ex.getMessage().contains("clone failed"));
+ verify(volumeDetailsDao, never()).addDetail(anyLong(), anyString(), anyString(), anyBoolean());
+ }
+ }
+
+ @Test
+ void testCopyAsync_CloneFailsWithCallback_CompletesWithFailure() {
+ SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(temporarySnapshotCopyVolume());
+ when(volumeInfo.getVolume()).thenReturn(temporarySnapshotCopyVolume());
+ stubSnapshotCloneSource(ProtocolType.ISCSI.name());
+ @SuppressWarnings("unchecked")
+ AsyncCompletionCallback copyCallback = mock(AsyncCompletionCallback.class);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any()))
+ .thenThrow(new CloudRuntimeException("clone failed"));
+
+ driver.copyAsync(snapshotInfo, volumeInfo, null, copyCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CopyCommandResult.class);
+ verify(copyCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ @Test
+ void testDeleteAsync_TemporarySnapshotCopy_NullCallbackRemovesRecord() {
+ VolumeVO tempVolume = temporarySnapshotCopyVolume();
+ tempVolume.setName("cs_tmp_snap_200_100");
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeDao.findById(100L)).thenReturn(tempVolume);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/cs_tmp_snap_200_100", false));
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "tmp-lun-uuid", false));
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
+
+ driver.deleteAsync(dataStore, volumeInfo, null);
+
+ verify(sanStrategy).deleteCloudStackVolume(any(CloudStackVolume.class));
+ verify(volumeDetailsDao).removeDetails(100L);
+ verify(volumeDao).remove(100L);
+ }
+ }
+
+ @Test
+ void testGrantAccess_TemporarySnapshotCopy_SyncsIscsiPathOnInMemoryVolume() {
+ String iscsiPath = "/iqn.1992-08.com.netapp:sn.123456/0";
+ VolumeVO dbVolume = mock(VolumeVO.class);
+ when(dbVolume.getId()).thenReturn(100L);
+ when(dbVolume.getState()).thenReturn(Volume.State.Allocated);
+ when(dbVolume.getDiskOfferingId()).thenReturn(0L);
+ when(dbVolume.getPath()).thenReturn(iscsiPath);
+ when(dbVolume.get_iScsiName()).thenReturn(iscsiPath);
+ VolumeVO inMemoryVolume = temporarySnapshotCopyVolume();
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getVolume()).thenReturn(inMemoryVolume);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getScope()).thenReturn(ScopeType.CLUSTER);
+ when(storagePool.getPath()).thenReturn("iqn.1992-08.com.netapp:sn.123456");
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(dbVolume);
+ when(host.getUuid()).thenReturn("host-uuid-1");
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/cs_tmp_snap_200_100", false));
+
+ AccessGroup existingAccessGroup = new AccessGroup();
+ Igroup existingIgroup = new Igroup();
+ existingIgroup.setName("igroup1");
+ existingAccessGroup.setIgroup(existingIgroup);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
+ utilityMock.when(() -> OntapStorageUtils.getIgroupName(anyString(), anyString())).thenReturn("igroup1");
+ when(sanStrategy.getAccessGroup(any())).thenReturn(existingAccessGroup);
+ when(sanStrategy.ensureLunMapped(anyString(), anyString(), anyString())).thenReturn("0");
+
+ assertTrue(driver.grantAccess(volumeInfo, host, dataStore));
+
+ assertEquals(iscsiPath, inMemoryVolume.getPath());
+ assertEquals(iscsiPath, inMemoryVolume.get_iScsiName());
+ }
+ }
+
}
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
index 38b7264b1f9a..814977bd0fda 100755
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
@@ -646,6 +646,43 @@ public void testCloneCloudStackVolume_Success() {
assertEquals("volume-uuid", captor.getValue().getDestinationPath());
assertEquals("flexvol1", captor.getValue().getVolume().getName());
assertEquals("flexvol-uuid-1", captor.getValue().getVolume().getUuid());
+ assertNull(captor.getValue().getSnapshot());
+ }
+
+ @Test
+ public void testCloneCloudStackVolume_SnapshotSource_SendsSnapshotName() {
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ when(volumeObject.getId()).thenReturn(100L);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDao.update(anyLong(), any(VolumeVO.class))).thenReturn(true);
+
+ Map details = new HashMap<>();
+ details.put(OntapStorageConstants.VOLUME_NAME, "flexvol1");
+ details.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-1");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(details);
+
+ FileInfo source = new FileInfo();
+ source.setPath("source-file-uuid");
+ CloudStackVolume request = new CloudStackVolume();
+ request.setDatastoreId("1");
+ request.setVolumeInfo(volumeObject);
+ request.setFile(source);
+ request.setDestinationPath("new-volume-uuid");
+ request.setSnapshotName("snap_cs200");
+
+ when(nasFeignClient.cloneFile(anyString(), any(FileCloneRequest.class))).thenReturn(new JobResponse());
+
+ CloudStackVolume result = strategy.cloneCloudStackVolume(request);
+
+ assertEquals("new-volume-uuid", result.getFile().getPath());
+ assertEquals("snap_cs200", result.getSnapshotName());
+ ArgumentCaptor captor = ArgumentCaptor.forClass(FileCloneRequest.class);
+ verify(nasFeignClient).cloneFile(anyString(), captor.capture());
+ assertEquals("source-file-uuid", captor.getValue().getSourcePath());
+ assertEquals("new-volume-uuid", captor.getValue().getDestinationPath());
+ assertNotNull(captor.getValue().getSnapshot());
+ assertEquals("snap_cs200", captor.getValue().getSnapshot().getName());
}
@Test
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 8acf6b0f8276..02b1e5263d50 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
@@ -75,6 +75,9 @@ class UnifiedSANStrategyTest {
@Mock
private SANFeignClient sanFeignClient;
+ @Mock
+ private org.apache.cloudstack.storage.feign.client.NASFeignClient nasFeignClient;
+
@Mock
private OntapStorage ontapStorage;
@@ -105,6 +108,10 @@ void setUp() {
sanFeignClientField.setAccessible(true);
sanFeignClientField.set(unifiedSANStrategy, sanFeignClient);
+ java.lang.reflect.Field nasFeignClientField = StorageStrategy.class.getDeclaredField("nasFeignClient");
+ nasFeignClientField.setAccessible(true);
+ nasFeignClientField.set(unifiedSANStrategy, nasFeignClient);
+
// Also inject the storage field from parent class to ensure proper mocking
java.lang.reflect.Field storageField = StorageStrategy.class.getDeclaredField("storage");
storageField.setAccessible(true);
@@ -1034,6 +1041,40 @@ void testCloneCloudStackVolume_MissingSource_ThrowsException() {
() -> unifiedSANStrategy.cloneCloudStackVolume(request));
}
+ @Test
+ void testCloneCloudStackVolume_SnapshotSource_SendsSnapshotQualifiedName() {
+ Lun.Source source = new Lun.Source();
+ source.setName("/vol/vol1/.snapshot/snap_cs200/source_lun");
+ Lun.Clone clone = new Lun.Clone();
+ clone.setSource(source);
+ Lun lun = new Lun();
+ lun.setName("/vol/vol1/new_lun");
+ lun.setClone(clone);
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lun);
+
+ Lun createdLun = new Lun();
+ createdLun.setName("/vol/vol1/new_lun");
+ createdLun.setUuid("new-lun-uuid");
+ OntapResponse response = new OntapResponse<>();
+ response.setRecords(List.of(createdLun));
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
+ .thenReturn(authHeader);
+ when(sanFeignClient.createLun(eq(authHeader), eq(true), any(Lun.class))).thenReturn(response);
+
+ CloudStackVolume result = unifiedSANStrategy.cloneCloudStackVolume(request);
+
+ assertEquals("new-lun-uuid", result.getLun().getUuid());
+ ArgumentCaptor lunCaptor = ArgumentCaptor.forClass(Lun.class);
+ verify(sanFeignClient).createLun(eq(authHeader), eq(true), lunCaptor.capture());
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun",
+ lunCaptor.getValue().getClone().getSource().getName());
+ assertNull(lunCaptor.getValue().getClone().getSource().getUuid());
+ }
+ }
+
@Test
void testResizeCloudStackVolume_ValidRequest_PatchesSize() {
Lun lun = new Lun();
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/utils/OntapStorageUtilsTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/utils/OntapStorageUtilsTest.java
index 372a75ad257d..3fe9be596aa0 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/utils/OntapStorageUtilsTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/utils/OntapStorageUtilsTest.java
@@ -79,4 +79,28 @@ public void getIgroupName_truncates_whenOneCharOverMaxLength() {
assertEquals(OntapStorageConstants.IGROUP_NAME_MAX_LENGTH, result.length());
}
+ @Test
+ public void toFlexVolRelativePath_stripsVolPrefix() {
+ assertEquals("lun1", OntapStorageUtils.toFlexVolRelativePath("/vol/vol1/lun1", "vol1"));
+ assertEquals("file-uuid", OntapStorageUtils.toFlexVolRelativePath("file-uuid", "vol1"));
+ assertEquals("file-uuid", OntapStorageUtils.toFlexVolRelativePath("/file-uuid", "vol1"));
+ }
+
+ @Test
+ public void toLunCloneSourcePathInSnapshot_buildsSnapshotQualifiedPath() {
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun",
+ OntapStorageUtils.toLunCloneSourcePathInSnapshot("/vol/vol1/source_lun", "vol1", "snap_cs200"));
+ assertEquals("/vol/vol1/.snapshot/snap_cs200/source_lun",
+ OntapStorageUtils.toLunCloneSourcePathInSnapshot("source_lun", "vol1", "snap_cs200"));
+ }
+
+ @Test
+ public void toLunCloneSourcePathInSnapshot_rejectsBlankInputs() {
+ org.junit.jupiter.api.Assertions.assertThrows(com.cloud.exception.InvalidParameterValueException.class,
+ () -> OntapStorageUtils.toLunCloneSourcePathInSnapshot("/vol/vol1/lun", "vol1", null));
+ org.junit.jupiter.api.Assertions.assertThrows(com.cloud.exception.InvalidParameterValueException.class,
+ () -> OntapStorageUtils.toLunCloneSourcePathInSnapshot("/vol/vol1/lun", "", "snap"));
+ org.junit.jupiter.api.Assertions.assertThrows(com.cloud.exception.InvalidParameterValueException.class,
+ () -> OntapStorageUtils.toLunCloneSourcePathInSnapshot("", "vol1", "snap"));
+ }
}
From 96009c31269ca3d3ea9da4699e9e4fef713ccac4 Mon Sep 17 00:00:00 2001
From: Surya Gupta
Date: Thu, 8 Oct 2026 16:43:57 +0530
Subject: [PATCH 5/9] QoS Support (Capacity IOPS on the pool, Create Offering
with QoS and Resize Changes for IOPS) (#102)
QoS Support includes:
1- Capacity IOPS on the pool,
2- Create Disk/Compute offering with QoS
3- Resize changes for QoS
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] New feature (non-breaking change which adds functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Enhancement (improves an existing feature and functionality)
- [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
- [ ] Build/CI
- [ ] Test (unit or integration test code)
- [x] Major
- [ ] Minor
- [ ] BLOCKER
- [ ] Critical
- [x] Major
- [ ] Minor
- [ ] Trivial
Performed the below testing for ISCSI and NFS both
1- Created the compute/disk offering with CustomIOPS and Fix IOPS both
(min 2000, max 10000)
2- Created a Storage Pool with Total IOPS 5000 - It will just store the
value in DB
3- Create a VM with Fix IOPS (ROOT + 1 DATA) - Created Lun/File, QoS
policy got created and attached to file/lun
4- Create a VM with Custom IOPS (min 2000, max 5000) (ROOT + 1 DATA) -
Created Lun/File, new QoS policy got created and attached to file/lun
5- Create a VM with Fix IOPS (ROOT + 1 DATA) again - Failed with error
saying there is no more IOPS on a pool
6- Change the Total IOPS on pool - worked and new VM creation also went
fine.
7- Resize the Volume, QoS existing field got populated and changed the
min and max value - that also worked by creating new QoS if not found
and attached to the file/Lun
8- Tested on FAS platform where Min QoS is set 1000 and it throw the
error in logs, not on UI as UI is not using the thrown message from
vendor
9- Tested on FAS platform where Min QoS is set 0 and VM created
successfully.
10- Tested with No QoS - Success
change?
---------
Co-authored-by: Gupta, Surya
---
.../java/com/cloud/vm/VmDetailConstants.java | 4 +
.../admin/storage/UpdateStoragePoolCmd.java | 3 +-
.../orchestration/CloudOrchestrator.java | 8 +-
.../orchestration/CloudOrchestratorTest.java | 111 ++
.../driver/OntapPrimaryDatastoreDriver.java | 515 +++++---
.../storage/feign/client/QosFeignClient.java | 51 +
.../storage/feign/client/SANFeignClient.java | 4 +-
.../storage/feign/model/FileInfo.java | 11 +
.../cloudstack/storage/feign/model/Lun.java | 11 +
.../storage/feign/model/VolumeQosPolicy.java | 93 +-
.../OntapPrimaryDatastoreLifecycle.java | 70 +-
.../storage/service/StorageStrategy.java | 347 ++++-
.../storage/service/UnifiedNASStrategy.java | 41 +-
.../storage/service/UnifiedSANStrategy.java | 35 +-
.../storage/utils/OntapStorageConstants.java | 10 +
.../OntapPrimaryDatastoreDriverTest.java | 1148 ++++++++++++++---
.../OntapPrimaryDatastoreLifecycleTest.java | 258 ++++
.../storage/service/StorageStrategyTest.java | 335 ++++-
.../service/UnifiedNASStrategyTest.java | 103 ++
.../service/UnifiedSANStrategyTest.java | 90 +-
ui/src/views/infra/AddPrimaryStorage.vue | 6 +
.../infra/zone/ZoneWizardAddResources.vue | 2 +-
.../views/infra/zone/ZoneWizardLaunchZone.vue | 3 +
23 files changed, 2782 insertions(+), 477 deletions(-)
create mode 100644 engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/CloudOrchestratorTest.java
create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/QosFeignClient.java
diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java
index 521633a0c597..13b6d28e6ed3 100644
--- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java
+++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java
@@ -70,6 +70,10 @@ public interface VmDetailConstants {
String CPU_SPEED = "cpuSpeed";
String MEMORY = "memory";
+ // VM deployment with custom root disk offering params
+ String MIN_IOPS = "minIops";
+ String MAX_IOPS = "maxIops";
+
// Misc details for internal usage (not to be set/changed by user or admin)
String CPU_OVER_COMMIT_RATIO = "cpuOvercommitRatio";
String MEMORY_OVER_COMMIT_RATIO = "memoryOvercommitRatio";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java
index fa55b424eaf3..31e72cb2ee7a 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/storage/UpdateStoragePoolCmd.java
@@ -53,7 +53,8 @@ public class UpdateStoragePoolCmd extends BaseCmd {
@Parameter(name = ApiConstants.TAGS, type = CommandType.LIST, collectionType = CommandType.STRING, description = "Comma-separated list of tags for the storage pool")
private List tags;
- @Parameter(name = ApiConstants.CAPACITY_IOPS, type = CommandType.LONG, required = false, description = "IOPS CloudStack can provision from this storage pool", validations={ApiArgValidator.PositiveNumber})
+ @Parameter(name = ApiConstants.CAPACITY_IOPS, type = CommandType.LONG, required = false, description = "IOPS CloudStack can provision from this storage pool",
+ validations = {ApiArgValidator.PositiveNumber})
private Long capacityIops;
@Parameter(name = ApiConstants.CAPACITY_BYTES, type = CommandType.LONG, required = false, description = "Bytes CloudStack can provision from this storage pool",validations={ApiArgValidator.PositiveNumber})
diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java
index 9f6d02cc1234..9a06e7285e4d 100644
--- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java
+++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/CloudOrchestrator.java
@@ -61,14 +61,12 @@
import com.cloud.vm.NicProfile;
import com.cloud.vm.VMInstanceVO;
import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.VmDetailConstants;
import com.cloud.vm.VmDiskInfo;
import com.cloud.vm.dao.UserVmDao;
import com.cloud.vm.dao.VMInstanceDetailsDao;
import com.cloud.vm.dao.VMInstanceDao;
-import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
-import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
-
@Component
public class CloudOrchestrator implements OrchestrationService {
@@ -205,8 +203,8 @@ public VirtualMachineEntity createVirtualMachine(String id, String owner, String
Map userVmDetails = _vmInstanceDetailsDao.listDetailsKeyPairs(vm.getId());
if (userVmDetails != null) {
- String minIops = userVmDetails.get(MIN_IOPS);
- String maxIops = userVmDetails.get(MAX_IOPS);
+ String minIops = userVmDetails.get(VmDetailConstants.MIN_IOPS);
+ String maxIops = userVmDetails.get(VmDetailConstants.MAX_IOPS);
rootDiskOfferingInfo.setMinIops(minIops != null && minIops.trim().length() > 0 ? Long.parseLong(minIops) : null);
rootDiskOfferingInfo.setMaxIops(maxIops != null && maxIops.trim().length() > 0 ? Long.parseLong(maxIops) : null);
diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/CloudOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/CloudOrchestratorTest.java
new file mode 100644
index 000000000000..fd7deac04548
--- /dev/null
+++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/CloudOrchestratorTest.java
@@ -0,0 +1,111 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.engine.orchestration;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import com.cloud.hypervisor.Hypervisor.HypervisorType;
+import com.cloud.offering.DiskOfferingInfo;
+import com.cloud.service.ServiceOfferingVO;
+import com.cloud.service.dao.ServiceOfferingDao;
+import com.cloud.storage.DiskOfferingVO;
+import com.cloud.storage.VMTemplateVO;
+import com.cloud.storage.dao.DiskOfferingDao;
+import com.cloud.storage.dao.VMTemplateDao;
+import com.cloud.utils.component.ComponentContext;
+import com.cloud.vm.VMInstanceVO;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.VmDetailConstants;
+import com.cloud.vm.dao.VMInstanceDao;
+import com.cloud.vm.dao.VMInstanceDetailsDao;
+import org.apache.cloudstack.engine.cloud.entity.api.VirtualMachineEntityImpl;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class CloudOrchestratorTest {
+
+ private static final long VM_ID = 1L;
+ private static final long SERVICE_OFFERING_ID = 2L;
+ private static final long ROOT_DISK_OFFERING_ID = 3L;
+ private static final String TEMPLATE_ID = "4";
+
+ @InjectMocks
+ private CloudOrchestrator cloudOrchestrator = new CloudOrchestrator();
+
+ @Mock
+ private VirtualMachineManager _itMgr;
+ @Mock
+ private VMTemplateDao _templateDao;
+ @Mock
+ private VMInstanceDao _vmDao;
+ @Mock
+ private VMInstanceDetailsDao _vmInstanceDetailsDao;
+ @Mock
+ private ServiceOfferingDao _serviceOfferingDao;
+ @Mock
+ private DiskOfferingDao _diskOfferingDao;
+
+ @Test
+ public void createVirtualMachineSetsCustomIopsFromVmDetails() throws Exception {
+ VMInstanceVO vm = Mockito.mock(VMInstanceVO.class);
+ ServiceOfferingVO serviceOffering = Mockito.mock(ServiceOfferingVO.class);
+ DiskOfferingVO rootDiskOffering = Mockito.mock(DiskOfferingVO.class);
+ VMTemplateVO template = Mockito.mock(VMTemplateVO.class);
+ VirtualMachineEntityImpl vmEntity = Mockito.mock(VirtualMachineEntityImpl.class);
+
+ Mockito.when(_vmDao.findByUuid("vm-uuid")).thenReturn(vm);
+ Mockito.when(vm.getId()).thenReturn(VM_ID);
+ Mockito.when(vm.getServiceOfferingId()).thenReturn(SERVICE_OFFERING_ID);
+ Mockito.when(vm.getInstanceName()).thenReturn("i-1-1-VM");
+ Mockito.when(_serviceOfferingDao.findById(VM_ID, SERVICE_OFFERING_ID)).thenReturn(serviceOffering);
+ Mockito.when(_diskOfferingDao.findById(ROOT_DISK_OFFERING_ID)).thenReturn(rootDiskOffering);
+ Mockito.when(rootDiskOffering.isCustomizedIops()).thenReturn(true);
+ Mockito.when(_templateDao.findById(Long.valueOf(TEMPLATE_ID))).thenReturn(template);
+
+ Map details = new HashMap<>();
+ details.put(VmDetailConstants.MIN_IOPS, "100");
+ details.put(VmDetailConstants.MAX_IOPS, "1000");
+ Mockito.when(_vmInstanceDetailsDao.listDetailsKeyPairs(VM_ID)).thenReturn(details);
+
+ try (MockedStatic componentContext = Mockito.mockStatic(ComponentContext.class)) {
+ componentContext.when(() -> ComponentContext.inject(VirtualMachineEntityImpl.class)).thenReturn(vmEntity);
+
+ cloudOrchestrator.createVirtualMachine("vm-uuid", "owner", TEMPLATE_ID, "host", "display", HypervisorType.KVM.name(),
+ 1, 1000, 1024, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyMap(), null,
+ null, null, null, null, ROOT_DISK_OFFERING_ID, null, null, null, null);
+ }
+
+ ArgumentCaptor rootDiskOfferingInfo = ArgumentCaptor.forClass(DiskOfferingInfo.class);
+ Mockito.verify(_itMgr).allocate(Mockito.eq("i-1-1-VM"), Mockito.eq(template), Mockito.eq(serviceOffering), rootDiskOfferingInfo.capture(),
+ Mockito.anyList(), Mockito.anyList(), Mockito.any(LinkedHashMap.class), Mockito.isNull(), Mockito.eq(HypervisorType.KVM),
+ Mockito.isNull(), Mockito.isNull(), Mockito.isNull(), Mockito.isNull());
+
+ Assert.assertEquals(Long.valueOf(100), rootDiskOfferingInfo.getValue().getMinIops());
+ Assert.assertEquals(Long.valueOf(1000), rootDiskOfferingInfo.getValue().getMaxIops());
+ }
+}
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 60fc8f4188ca..40598d5e12b6 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
@@ -28,15 +28,15 @@
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor.HypervisorType;
import com.cloud.storage.DataStoreRole;
-import com.cloud.storage.SnapshotVO;
+import com.cloud.storage.ResizeVolumePayload;
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.VMTemplateStoragePoolVO;
import com.cloud.storage.SnapshotVO;
+import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.dao.SnapshotDao;
import com.cloud.storage.dao.SnapshotDetailsDao;
import com.cloud.storage.dao.SnapshotDetailsVO;
@@ -71,10 +71,12 @@
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.feign.model.LunSpace;
import org.apache.cloudstack.storage.feign.model.Svm;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.SANStrategy;
import org.apache.cloudstack.storage.service.StorageStrategy;
+import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
@@ -92,6 +94,7 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Primary datastore driver for NetApp ONTAP storage systems.
@@ -115,6 +118,7 @@ public Map getCapabilities() {
Map mapCapabilities = new HashMap<>();
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
+ mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString());
// Enables the framework to cache a template on the FlexVolume once and serve every later
// deployment with an array-side clone instead of another copy from secondary storage.
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_VOLUME.toString(), Boolean.TRUE.toString());
@@ -123,7 +127,6 @@ public Map getCapabilities() {
// Must be present and false: StorageSystemDataMotionStrategy then clones the snapshot into a
// temporary volume (see copyAsync) and copies that volume to secondary storage.
mapCapabilities.put(OntapStorageConstants.CAN_DIRECT_ATTACH_SNAPSHOT, Boolean.FALSE.toString());
- mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString());
return mapCapabilities;
}
@@ -170,6 +173,7 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
}
Map details = storagePoolDetailsDao.listDetailsKeyPairs(dataStore.getId());
+ validateProtocol(details, dataStore);
if (dataObject.getType() == DataObjectType.VOLUME) {
VolumeInfo volInfo = (VolumeInfo) dataObject;
@@ -209,34 +213,6 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
storagePool, volInfo, details, cloneOfTemplateId);
} else {
clonedCloudStackVolume = createCloudStackVolume(storagePool, volInfo, details);
- // 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());
- volumeVO.setFormat(getImageFormatByHypervisorAndProtocol(storagePool.getHypervisor(), details.get(OntapStorageConstants.PROTOCOL)));
- logger.info("createAsync: Volume format set to [{}] for hypervisor [{}] and protocol [{}]", volumeVO.getFormat(), storagePool.getHypervisor(), details.get(OntapStorageConstants.PROTOCOL));
-
- if (ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
- String lunName = created != null && created.getLun() != null ? created.getLun().getName() : null;
- if (lunName == null) {
- throw new CloudRuntimeException("Missing LUN name for volume " + volInfo.getId());
- }
-
- // Persist LUN details for future operations (delete, grant/revoke access)
- volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_UUID, created.getLun().getUuid(), false);
- volumeDetailsDao.addDetail(volInfo.getId(), OntapStorageConstants.LUN_DOT_NAME, lunName, false);
- if (created.getLun().getUuid() != null) {
- volumeVO.setFolder(created.getLun().getUuid());
- }
-
- logger.info("createAsync: Created LUN [{}] for volume [{}]. LUN mapping will occur during grantAccess() to per-host igroup.",
- lunName, volumeVO.getId());
- createCmdResult = new CreateCmdResult(lunName, new Answer(null, true, null));
- } else if (ProtocolType.NFS3.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL))) {
- createCmdResult = new CreateCmdResult(volInfo.getUuid(), new Answer(null, true, null));
- logger.info("createAsync: Managed NFS volume [{}] with path [{}] associated with pool {}",
- volumeVO.getId(), volInfo.getUuid(), storagePool.getId());
}
createCmdResult = recordCreatedVolume(storagePool, volInfo, volumeVO, details, clonedCloudStackVolume);
@@ -252,7 +228,7 @@ public void createAsync(DataStore dataStore, DataObject dataObject, AsyncComplet
errMsg = e.getMessage();
logger.error("createAsync: Failed for dataObject name [{}]: {}", dataObject.getName(), errMsg);
createCmdResult = new CreateCmdResult(null, new Answer(null, false, errMsg));
- createCmdResult.setResult(e.toString());
+ createCmdResult.setResult(errMsg);
} finally {
if (createCmdResult != null && createCmdResult.isSuccess()) {
logger.info("createAsync: Operation completed successfully for {}", dataObject.getType());
@@ -300,8 +276,86 @@ private CreateCmdResult recordCreatedVolume(StoragePoolVO storagePool, VolumeInf
* Creates a volume on the ONTAP backend.
*/
private CloudStackVolume createCloudStackVolume(StoragePoolVO storagePool, VolumeInfo volumeObject, Map details) {
+ verifySufficientIopsForStoragePool(storagePool, volumeObject.getMinIops(), volumeObject.getId());
StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
- return storageStrategy.createCloudStackVolume(createVolumeRequest(storagePool, details, volumeObject));
+ VolumeQosPolicy qosPolicy = null;
+ Volume.Type volumeType = volumeObject.getVolumeType();
+ if (volumeType == Volume.Type.DATADISK || volumeType == Volume.Type.ROOT) {
+ qosPolicy = createQosPolicyIfNeeded(storageStrategy, details,
+ volumeObject.getMinIops(), volumeObject.getMaxIops(), storagePool.getId());
+ }
+ CloudStackVolume request = createCloudStackVolumeRequestByProtocol(
+ storagePool, details, volumeObject, qosPolicy);
+ try {
+ CloudStackVolume created = storageStrategy.createCloudStackVolume(request);
+ persistQosPolicyDetails(volumeObject.getId(), qosPolicy);
+ return created;
+ } catch (RuntimeException e) {
+ if (qosPolicy != null) {
+ storageStrategy.deleteVolumeQosPolicy(qosPolicy.getUuid());
+ }
+ throw e;
+ }
+ }
+
+ private VolumeQosPolicy createQosPolicyIfNeeded(StorageStrategy storageStrategy, Map details,
+ Long minIops, Long maxIops, Long poolId) {
+ if (!validateIops(storageStrategy, details, poolId, minIops, maxIops)) {
+ return null;
+ }
+ String policyName = getQosPolicyName(details.get(OntapStorageConstants.SVM_NAME), minIops, maxIops);
+ return storageStrategy.createVolumeQosPolicy(policyName, minIops, maxIops);
+ }
+
+ private boolean validateIops(StorageStrategy storageStrategy, Map details,
+ Long poolId, Long minIops, Long maxIops) {
+ long min = minIops == null ? 0 : minIops;
+ long max = maxIops == null ? 0 : maxIops;
+ if (min <= 0 && max <= 0) {
+ return false;
+ }
+ if (min > 0 && max > 0 && min > max) {
+ throw new CloudRuntimeException("Minimum IOPS cannot be greater than maximum IOPS");
+ }
+ if (min > 0) {
+ String isAff = details.get(OntapStorageConstants.IS_AFF);
+ if (StringUtils.isBlank(isAff)) {
+ try {
+ isAff = Boolean.toString(storageStrategy.isAff());
+ details.put(OntapStorageConstants.IS_AFF, isAff);
+ storagePoolDetailsDao.addDetail(poolId, OntapStorageConstants.IS_AFF, isAff, false);
+ } catch (Exception e) {
+ logger.error("Unable to determine whether storage pool [{}] is AFF or FAS: {}",
+ poolId, e.getMessage());
+ return true;
+ }
+ }
+ if (!Boolean.parseBoolean(isAff)) {
+ throw new CloudRuntimeException(OntapStorageConstants.MIN_IOPS_NOT_SUPPORTED_ON_FAS);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Builds a reusable SVM-scoped QoS policy name: cs_{min}_to_{max}_iops_{svmName}.
+ * Dots in the SVM name are replaced with underscores (ONTAP QoS names cannot contain '.').
+ */
+ private String getQosPolicyName(String svmName, Long minIops, Long maxIops) {
+ String sanitizedSvmName = svmName == null ? "" : svmName.replace(".", OntapStorageConstants.UNDERSCORE);
+ long min = minIops != null && minIops > 0 ? minIops : 0;
+ long max = maxIops != null && maxIops > 0 ? maxIops : 0;
+ return OntapStorageConstants.QOS_POLICY_NAME_PREFIX + min + OntapStorageConstants.UNDERSCORE
+ + OntapStorageConstants.QOS_POLICY_NAME_TO + max + OntapStorageConstants.UNDERSCORE
+ + OntapStorageConstants.QOS_POLICY_NAME_IOPS + sanitizedSvmName;
+ }
+
+ private void persistQosPolicyDetails(long volumeId, VolumeQosPolicy qosPolicy) {
+ volumeDetailsDao.removeDetail(volumeId, OntapStorageConstants.QOS_POLICY_UUID);
+ if (qosPolicy == null) {
+ return;
+ }
+ volumeDetailsDao.addDetail(volumeId, OntapStorageConstants.QOS_POLICY_UUID, qosPolicy.getUuid(), false);
}
/**
@@ -363,36 +417,47 @@ private CloudStackVolume cloneCloudStackVolumeFromTemplate(StoragePoolVO storage
StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
boolean iscsi = isIscsi(details);
- long templateSize = templatePoolRef.getTemplateSize();
- if (templateSize <= 0) {
- throw new CloudRuntimeException("Template cache size is missing or invalid (" + templateSize
- + ") for template [" + templateId + "] on pool [" + storagePool.getId()
- + "]; cannot clone volume [" + volumeInfo.getId() + "]");
+ verifySufficientIopsForStoragePool(storagePool, volumeInfo.getMinIops(), volumeInfo.getId());
+ VolumeQosPolicy qosPolicy = null;
+ Volume.Type volumeType = volumeInfo.getVolumeType();
+ if (volumeType == Volume.Type.DATADISK || volumeType == Volume.Type.ROOT) {
+ qosPolicy = createQosPolicyIfNeeded(storageStrategy, details,
+ volumeInfo.getMinIops(), volumeInfo.getMaxIops(), storagePool.getId());
}
CloudStackVolume request = iscsi
- ? createCloneLunRequest(storagePool, details, volumeInfo, templatePoolRef, templateId)
+ ? createCloneLunRequest(storagePool, details, volumeInfo, templatePoolRef, templateId, qosPolicy)
: createCloneFileRequest(storagePool, volumeInfo, templatePoolRef, templateId);
- CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request);
- // SAN cloneCloudStackVolume validates the Feign response (LUN name + uuid) before returning
- if (cloned == null) {
- throw new CloudRuntimeException("ONTAP returned nothing when cloning template [" + templateId
- + "] for volume [" + volumeInfo.getId() + "]");
- }
+ try {
+ CloudStackVolume cloned = storageStrategy.cloneCloudStackVolume(request);
+ if (cloned == null) {
+ throw new CloudRuntimeException("ONTAP returned nothing when cloning template [" + templateId
+ + "] for volume [" + volumeInfo.getId() + "]");
+ }
- logger.info("cloneCloudStackVolumeFromTemplate: Cloned template [{}] for volume [{}] on pool [{}]",
- templateId, volumeInfo.getId(), storagePool.getId());
+ logger.info("cloneCloudStackVolumeFromTemplate: Cloned template [{}] for volume [{}] on pool [{}]",
+ templateId, volumeInfo.getId(), storagePool.getId());
- long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool);
- if (requestedSize > templateSize) {
- logger.info("cloneCloudStackVolumeFromTemplate: Growing clone of template [{}] from {} to {} bytes for volume [{}]",
- templateId, templateSize, requestedSize, volumeInfo.getId());
- resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize,
- "cloneCloudStackVolumeFromTemplate template [" + templateId + "] volume [" + volumeInfo.getId() + "]");
- }
+ long requestedSize = getDataObjectSizeIncludingHypervisorSnapshotReserve(volumeInfo, storagePool);
+ if (requestedSize > templatePoolRef.getTemplateSize()) {
+ logger.info("cloneCloudStackVolumeFromTemplate: Growing clone of template [{}] from {} to {} bytes for volume [{}]",
+ templateId, templatePoolRef.getTemplateSize(), requestedSize, volumeInfo.getId());
+ resizeClonedVolumeOrCleanup(storageStrategy, cloned, requestedSize,
+ "cloneCloudStackVolumeFromTemplate template [" + templateId + "] volume [" + volumeInfo.getId() + "]");
+ }
- return cloned;
+ if (!iscsi && qosPolicy != null) {
+ attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, qosPolicy);
+ }
+ persistQosPolicyDetails(volumeInfo.getId(), qosPolicy);
+ return cloned;
+ } catch (Exception e) {
+ if (qosPolicy != null) {
+ storageStrategy.deleteVolumeQosPolicy(qosPolicy.getUuid());
+ }
+ throw e;
+ }
}
/**
@@ -406,12 +471,7 @@ private Long getTemplateIdForCloning(long volumeId) {
if (detail == null || detail.getValue() == null || detail.getValue().isEmpty()) {
return null;
}
- try {
- return Long.valueOf(detail.getValue());
- } catch (NumberFormatException e) {
- throw new CloudRuntimeException("Stored cloneOfTemplate value [" + detail.getValue()
- + "] for volume [" + volumeId + "] is not a template id", e);
- }
+ return Long.valueOf(detail.getValue());
}
/**
@@ -646,11 +706,33 @@ private void deleteNfsTemplateCache(Map details, TemplateInfo te
+ "cannot delete NFS template cache file [" + filePath + "] for template ["
+ templateInfo.getId() + "]");
}
- storageStrategy.deleteFileByPath(flexVolUuid, filePath);
+ ((UnifiedNASStrategy) storageStrategy).deleteFileByPath(flexVolUuid, filePath);
logger.info("deleteNfsTemplateCache: Deleted template cache file [{}] for template [{}]",
filePath, templateInfo.getId());
}
+ /**
+ * Rejects the request when the minimum IOPS being asked for would push the pool past its
+ * configured IOPS capacity. Pools without an IOPS capacity enforce no ceiling.
+ *
+ * Used IOPS include volumes still in {@link Volume.State#Creating} so a second overlapping
+ * create sees the first reservation. The volume being checked is omitted so its own min IOPS
+ * are not counted twice (CloudStack has already moved it to Creating before this runs).
+ */
+ private void verifySufficientIopsForStoragePool(StoragePoolVO storagePool, Long requestedMinIops, long excludeVolumeId) {
+ Long capacityIops = storagePool.getCapacityIops();
+ if (capacityIops == null || requestedMinIops == null || requestedMinIops <= 0) {
+ return;
+ }
+
+ long requestedTotalIops = getAllocatedMinIops(storagePool, excludeVolumeId) + requestedMinIops;
+ if (requestedTotalIops > capacityIops) {
+ throw new CloudRuntimeException(String.format(
+ "Insufficient IOPS capacity on storage pool %s: requested total of %d IOPS exceeds the pool IOPS capacity of %d",
+ storagePool.getName(), requestedTotalIops, capacityIops));
+ }
+ }
+
/**
* Deletes a volume or snapshot from the ONTAP storage system.
*
@@ -681,8 +763,13 @@ 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;
+ VolumeDetailVO qosPolicyDetail = volumeDetailsDao.findDetail(
+ volumeInfo.getId(), OntapStorageConstants.QOS_POLICY_UUID);
CloudStackVolume cloudStackVolumeRequest = createDeleteCloudStackVolumeRequest(storagePool, details, volumeInfo);
storageStrategy.deleteCloudStackVolume(cloudStackVolumeRequest);
+ if (qosPolicyDetail != null) {
+ deleteUnusedQosPolicy(storageStrategy, qosPolicyDetail.getValue());
+ }
logger.info("deleteAsync: Volume deleted: " + volumeInfo.getId());
removeTemporarySnapshotCopyRecord(volumeInfo.getId());
commandResult.setResult(null);
@@ -936,8 +1023,111 @@ private void removeTemporarySnapshotCopyRecord(long volumeId) {
logger.info("deleteAsync: Removed temporary snapshot copy volume record [{}]", volumeId);
}
+ /**
+ * Applies IOPS/QoS on resize. Capacity (LUN/file size) is not updated here.
+ * TODO: apply volume size change once size-resize support is merged.
+ */
@Override
- public void resize(DataObject data, AsyncCompletionCallback callback) {}
+ public void resize(DataObject data, AsyncCompletionCallback callback) {
+ String errMsg = null;
+ String path = null;
+ try {
+ if (!(data instanceof VolumeInfo)) {
+ throw new CloudRuntimeException("Invalid DataObjectType ("
+ + (data != null ? data.getType() : null) + ") passed to resize");
+ }
+ VolumeInfo volumeInfo = (VolumeInfo) data;
+ path = volumeInfo.getPath();
+ applyVolumeQos(volumeInfo);
+ } catch (Exception e) {
+ errMsg = e.getMessage();
+ logger.error("Failed to update IOPS for volume [{}]: {}", data != null ? data.getId() : null, errMsg, e);
+ }
+
+ CreateCmdResult result = new CreateCmdResult(path, new Answer(null, errMsg == null, errMsg));
+ result.setResult(errMsg);
+ callback.complete(result);
+ }
+
+ private void applyVolumeQos(VolumeInfo volumeInfo) {
+ ResizeVolumePayload payload = (ResizeVolumePayload) volumeInfo.getpayload();
+ if (payload == null) {
+ throw new CloudRuntimeException("Missing resize payload for volume " + volumeInfo.getId());
+ }
+ VolumeVO volume = volumeDao.findById(volumeInfo.getId());
+ if (volume == null || volume.getPoolId() == null) {
+ throw new CloudRuntimeException("Unable to resolve volume or storage pool for IOPS update");
+ }
+ StoragePoolVO storagePool = storagePoolDao.findById(volume.getPoolId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage pool not found for volume " + volume.getId());
+ }
+
+ verifySufficientIopsForStoragePool(storagePool, payload.newMinIops, volume.getId());
+
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ VolumeDetailVO qosDetail = volumeDetailsDao.findDetail(volume.getId(), OntapStorageConstants.QOS_POLICY_UUID);
+ String previousUuid = qosDetail != null ? qosDetail.getValue() : null;
+ VolumeQosPolicy qosPolicy = createQosPolicyIfNeeded(storageStrategy, details,
+ payload.newMinIops, payload.newMaxIops, volume.getPoolId());
+
+ if (qosPolicy != null && Objects.equals(previousUuid, qosPolicy.getUuid())) {
+ return;
+ }
+ if (qosPolicy != null) {
+ try {
+ attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, qosPolicy);
+ persistQosPolicyDetails(volume.getId(), qosPolicy);
+ } catch (RuntimeException e) {
+ deleteUnusedQosPolicy(storageStrategy, qosPolicy.getUuid());
+ throw e;
+ }
+ deleteUnusedQosPolicy(storageStrategy, previousUuid);
+ return;
+ }
+ if (previousUuid != null) {
+ detachQosPolicy(storageStrategy, storagePool, details, volumeInfo);
+ persistQosPolicyDetails(volume.getId(), null);
+ deleteUnusedQosPolicy(storageStrategy, previousUuid);
+ }
+ }
+
+ private void deleteUnusedQosPolicy(StorageStrategy storageStrategy, String policyUuid) {
+ if (policyUuid == null || policyUuid.isEmpty()) {
+ return;
+ }
+ try {
+ storageStrategy.deleteVolumeQosPolicy(policyUuid);
+ } catch (Exception e) {
+ logger.error("Unused QoS policy [{}] was not deleted: {}", policyUuid, e.getMessage());
+ }
+ }
+
+ private void attachQosPolicy(StorageStrategy storageStrategy, StoragePoolVO storagePool,
+ Map details, VolumeInfo volumeInfo,
+ VolumeQosPolicy qosPolicy) {
+ CloudStackVolume request = createCloudStackVolumeRequestByProtocol(
+ storagePool, details, volumeInfo, qosPolicy);
+ if (isIscsi(details)) {
+ VolumeDetailVO lunUuid = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.LUN_DOT_UUID);
+ if (lunUuid == null || lunUuid.getValue() == null) {
+ throw new CloudRuntimeException("LUN UUID is missing for volume " + volumeInfo.getId());
+ }
+ if (request.getLun() == null) {
+ throw new CloudRuntimeException("Missing LUN on QoS update request for volume " + volumeInfo.getId());
+ }
+ request.getLun().setUuid(lunUuid.getValue());
+ }
+ storageStrategy.updateCloudStackVolume(request);
+ }
+
+ private void detachQosPolicy(StorageStrategy storageStrategy, StoragePoolVO storagePool,
+ Map details, VolumeInfo volumeInfo) {
+ VolumeQosPolicy noPolicy = new VolumeQosPolicy();
+ noPolicy.setName(OntapStorageConstants.QOS_POLICY_NONE);
+ attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, noPolicy);
+ }
@Override
public ChapInfo getChapInfo(DataObject dataObject) {
@@ -1381,9 +1571,31 @@ public long getUsedBytes(StoragePool storagePool) {
return 0;
}
+ /**
+ * Returns min IOPS reserved on the pool, including volumes still being created.
+ * Destroyed and expunged volumes are omitted by the DAO query.
+ */
@Override
public long getUsedIops(StoragePool storagePool) {
- return 0;
+ return getAllocatedMinIops(storagePool, null);
+ }
+
+ private long getAllocatedMinIops(StoragePool storagePool, Long excludeVolumeId) {
+ long usedIops = 0;
+
+ List volumes = volumeDao.findNonDestroyedVolumesByPoolId(storagePool.getId(), null);
+ if (volumes != null) {
+ for (VolumeVO volume : volumes) {
+ if (excludeVolumeId != null && excludeVolumeId.equals(volume.getId())) {
+ continue;
+ }
+ if (volume.getMinIops() != null) {
+ usedIops += volume.getMinIops();
+ }
+ }
+ }
+
+ return usedIops;
}
/**
@@ -1480,7 +1692,7 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback snapshotDetails = new LinkedHashMap<>();
snapshotDetails.put(OntapStorageConstants.SRC_CS_VOLUME_ID, String.valueOf(volumeInfo.getId()));
@@ -1496,9 +1708,6 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback details) {
}
/**
- * Builds the request that creates a blank volume (LUN for iSCSI, qcow2 file for NFS).
+ * Builds the request that creates or updates a volume (LUN for iSCSI, qcow2 file for NFS),
+ * attaching a QoS policy reference when one is provided.
*/
- private CloudStackVolume createVolumeRequest(StoragePoolVO storagePool, Map details, DataObject volumeObject) {
- CloudStackVolume request = new CloudStackVolume();
+ private CloudStackVolume createCloudStackVolumeRequestByProtocol(StoragePoolVO storagePool, Map details,
+ DataObject volumeObject, VolumeQosPolicy qosPolicy) {
+ VolumeQosPolicy qosPolicyReference = null;
+ if (qosPolicy != null) {
+ qosPolicyReference = new VolumeQosPolicy();
+ qosPolicyReference.setName(qosPolicy.getName());
+ qosPolicyReference.setUuid(qosPolicy.getUuid());
+ }
+
String protocol = details.get(OntapStorageConstants.PROTOCOL);
- if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) {
- request.setDatastoreId(String.valueOf(storagePool.getId()));
- request.setVolumeInfo(volumeObject);
- } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) {
- Lun lunRequest = new Lun();
- Svm svm = new Svm();
- svm.setName(details.get(OntapStorageConstants.SVM_NAME));
- String lunName = volumeObject.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
- if (!OntapStorageUtils.isValidName(lunName)) {
- throw new InvalidParameterValueException("Invalid dataObject name [" + lunName
- + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
- }
- lunRequest.setSvm(svm);
- lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
- lunRequest.setOsType(Lun.OsTypeEnum.valueOf(OntapStorageUtils.getOSTypeFromHypervisor(storagePool.getHypervisor().name())));
- LunSpace lunSpace = new LunSpace();
- lunSpace.setSize(volumeObject.getSize());
- lunRequest.setSpace(lunSpace);
- request.setLun(lunRequest);
- } else {
- throw new CloudRuntimeException("Unsupported protocol " + protocol);
+ ProtocolType protocolType = ProtocolType.valueOf(protocol);
+ switch (protocolType) {
+ case NFS3:
+ CloudStackVolume nfsRequest = new CloudStackVolume();
+ nfsRequest.setDatastoreId(String.valueOf(storagePool.getId()));
+ nfsRequest.setFlexVolumeUuid(details.get(OntapStorageConstants.VOLUME_UUID));
+ nfsRequest.setVolumeInfo(volumeObject);
+ if (qosPolicyReference != null) {
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(qosPolicyReference);
+ nfsRequest.setFile(fileInfo);
+ }
+ return nfsRequest;
+ case ISCSI:
+ Svm svm = new Svm();
+ svm.setName(details.get(OntapStorageConstants.SVM_NAME));
+ CloudStackVolume iscsiRequest = new CloudStackVolume();
+ Lun lunRequest = new Lun();
+ lunRequest.setSvm(svm);
+
+ LunSpace lunSpace = new LunSpace();
+ lunSpace.setSize(volumeObject.getSize());
+ lunRequest.setSpace(lunSpace);
+ String lunName = volumeObject.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
+ if (!OntapStorageUtils.isValidName(lunName)) {
+ throw new InvalidParameterValueException("Invalid dataObject name [" + lunName
+ + "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
+ }
+ lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
+ lunRequest.setOsType(Lun.OsTypeEnum.valueOf(
+ OntapStorageUtils.getOSTypeFromHypervisor(storagePool.getHypervisor().name())));
+ lunRequest.setQosPolicy(qosPolicyReference);
+ iscsiRequest.setLun(lunRequest);
+ return iscsiRequest;
+ default:
+ throw new CloudRuntimeException("Unsupported protocol " + protocol);
}
- return request;
}
/**
@@ -1791,7 +2022,7 @@ private String getTemplateLunName(StoragePoolVO storagePool, long templateId) {
*/
private CloudStackVolume createCloneLunRequest(StoragePoolVO storagePool, Map details,
VolumeInfo volumeObject, VMTemplateStoragePoolVO templatePoolRef,
- long templateId) {
+ long templateId, VolumeQosPolicy qosPolicy) {
String sourceLunUuid = templatePoolRef.getLocalDownloadPath();
if (sourceLunUuid == null || sourceLunUuid.isEmpty()) {
throw new CloudRuntimeException("Template [" + templateId + "] has no cached LUN on pool ["
@@ -1817,6 +2048,12 @@ private CloudStackVolume createCloneLunRequest(StoragePoolVO storagePool, Map details, DataStore dataStore) {
+ String protocol = details == null ? null : details.get(OntapStorageConstants.PROTOCOL);
+ boolean supported = protocol != null && Arrays.stream(ProtocolType.values())
+ .anyMatch(type -> type.name().equalsIgnoreCase(protocol));
+ if (!supported) {
+ throw new CloudRuntimeException("Unsupported protocol [" + protocol + "] on storage pool ["
+ + dataStore.getName() + "]; supported protocols are " + Arrays.toString(ProtocolType.values()));
+ }
+ }
- private Storage.ImageFormat getImageFormatByHypervisorAndProtocol(HypervisorType hypervisorType, String protocol) {
- if (HypervisorType.KVM.equals(hypervisorType)) {
- ProtocolType protocolType = ProtocolType.valueOf(protocol);
- switch (protocolType) {
- case NFS3:
- return Storage.ImageFormat.QCOW2;
- case ISCSI:
- return Storage.ImageFormat.RAW;
- default:
- throw new CloudRuntimeException("Unsupported protocol [" + protocol + "] for ONTAP image format resolution");
- }
+ private Storage.ImageFormat getImageFormat(StoragePoolVO storagePool) {
+ HypervisorType hypervisorType = storagePool.getHypervisor();
+ if (!HypervisorType.KVM.equals(hypervisorType)) {
+ throw new CloudRuntimeException("Unsupported hypervisor [" + hypervisorType + "] for ONTAP image format resolution");
+ }
+ Storage.StoragePoolType spType = storagePool.getPoolType();
+ switch (spType) {
+ case OntapiSCSI:
+ return Storage.ImageFormat.RAW;
+ case NetworkFilesystem:
+ return Storage.ImageFormat.QCOW2;
+ default:
+ throw new CloudRuntimeException("Unsupported pool type [" + spType + "] for ONTAP image format resolution");
}
- throw new CloudRuntimeException("Unsupported hypervisor [" + hypervisorType + "] for ONTAP image format resolution");
}
+
/**
* Persists ONTAP snapshot metadata in {@code snapshot_details} for revert, delete, and
* create-volume-from-snapshot.
*
* Volume-snapshot delete reads {@code base_ontap_fv_id} and {@code ontap_snap_id} here
- * during {@link #deleteOntapSnapshot}; missing rows prevent ONTAP cleanup.
+ * during {@link #deleteCloudStackVolumeSnapshot}; missing rows prevent ONTAP cleanup.
*
* All rows go through {@link #persistSnapshotDetail} so DAO writes stay consistent;
* optional fields (LUN uuid, IOPS) are skipped when unset.
@@ -1953,20 +2205,6 @@ private void updateSnapshotDetails(long csSnapshotId, Map detail
/**
* Adds an IOPS entry to {@code details} only when {@code iops} is non-null and positive.
- * Persists ONTAP snapshot metadata in {@code snapshot_details} for revert and delete.
- *
- * Volume-snapshot delete reads {@code base_ontap_fv_id} and {@code ontap_snap_id} here
- * during {@link #deleteCloudStackVolumeSnapshot}; missing rows prevent ONTAP cleanup.
- *
- * @param csSnapshotId CloudStack snapshot ID
- * @param csVolumeId Source CloudStack volume ID
- * @param flexVolUuid ONTAP FlexVolume UUID
- * @param ontapSnapshotUuid ONTAP FlexVolume snapshot UUID
- * @param snapshotName ONTAP snapshot name
- * @param volumePath Path of the volume file/LUN within the FlexVolume (for restore)
- * @param storagePoolId Primary storage pool ID
- * @param protocol Storage protocol (NFS3 or ISCSI)
- * @param lunUuid LUN UUID (only for iSCSI, null for NFS)
*/
private static void putPositiveIopsDetail(Map details, String detailName, Long iops) {
if (iops != null && iops > 0) {
@@ -1985,39 +2223,4 @@ private void persistSnapshotDetail(long csSnapshotId, String detailName, String
snapshotDetailsDao.persist(new SnapshotDetailsVO(csSnapshotId, detailName, value, false));
}
- /**
- * Only ISCSI and NFS3 pools can be provisioned. Any other protocol would fall through
- * create and copy without producing a backend object.
- */
- private void validateProtocol(Map details, DataStore dataStore) {
- String protocol = details == null ? null : details.get(OntapStorageConstants.PROTOCOL);
- boolean supported = protocol != null && Arrays.stream(ProtocolType.values())
- .anyMatch(type -> type.name().equalsIgnoreCase(protocol));
- if (!supported) {
- throw new CloudRuntimeException("Unsupported protocol [" + protocol + "] on storage pool ["
- + dataStore.getName() + "]; supported protocols are " + Arrays.toString(ProtocolType.values()));
- }
- }
-
- /**
- * Image format stored on the CloudStack volume. This branch's iSCSI pools use
- * {@link Storage.StoragePoolType#Iscsi}; NFS pools use {@link Storage.StoragePoolType#NetworkFilesystem}.
- */
- private Storage.ImageFormat getImageFormat(StoragePoolVO storagePool) {
- HypervisorType hypervisorType = storagePool.getHypervisor();
- if (!HypervisorType.KVM.equals(hypervisorType)) {
- throw new CloudRuntimeException("Unsupported hypervisor [" + hypervisorType + "] for ONTAP image format resolution");
- }
- Storage.StoragePoolType spType = storagePool.getPoolType();
- switch (spType) {
- case Iscsi:
- case IscsiLUN:
- return Storage.ImageFormat.RAW;
- case NetworkFilesystem:
- return Storage.ImageFormat.QCOW2;
- default:
- throw new CloudRuntimeException("Unsupported pool type [" + spType + "] for ONTAP image format resolution");
- }
- }
-
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/QosFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/QosFeignClient.java
new file mode 100644
index 000000000000..669ea935124d
--- /dev/null
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/QosFeignClient.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.cloudstack.storage.feign.client;
+
+import java.util.Map;
+
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
+import org.apache.cloudstack.storage.feign.model.response.JobResponse;
+import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
+
+import feign.Headers;
+import feign.Param;
+import feign.QueryMap;
+import feign.RequestLine;
+
+public interface QosFeignClient {
+
+ @RequestLine("POST /api/storage/qos/policies")
+ @Headers({"Authorization: {authHeader}"})
+ JobResponse createPolicy(@Param("authHeader") String authHeader, VolumeQosPolicy policy);
+
+ @RequestLine("GET /api/storage/qos/policies")
+ @Headers({"Authorization: {authHeader}"})
+ OntapResponse getPolicies(@Param("authHeader") String authHeader,
+ @QueryMap Map queryParams);
+
+ @RequestLine("GET /api/storage/qos/policies/{uuid}")
+ @Headers({"Authorization: {authHeader}"})
+ VolumeQosPolicy getPolicy(@Param("authHeader") String authHeader, @Param("uuid") String uuid,
+ @QueryMap Map queryParams);
+
+ @RequestLine("DELETE /api/storage/qos/policies/{uuid}")
+ @Headers({"Authorization: {authHeader}"})
+ JobResponse deletePolicy(@Param("authHeader") String authHeader, @Param("uuid") String uuid);
+}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
index d365468cee10..8a60a9a3e2bb 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SANFeignClient.java
@@ -52,11 +52,11 @@ public interface SANFeignClient {
@RequestLine("PATCH /api/storage/luns/{uuid}")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
- void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun);
+ JobResponse updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun);
@RequestLine("DELETE /api/storage/luns/{uuid}")
@Headers({"Authorization: {authHeader}"})
- void deleteLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, @QueryMap Map queryMap);
+ JobResponse deleteLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, @QueryMap Map queryMap);
// iGroup Operation APIs
@RequestLine("POST /api/protocols/san/igroups?return_records={returnRecords}")
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileInfo.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileInfo.java
index a5dd24a3a286..71bf4c980bd4 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileInfo.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileInfo.java
@@ -49,6 +49,8 @@ public class FileInfo {
private Boolean overwriteEnabled = null;
@JsonProperty("path")
private String path = null;
+ @JsonProperty("qos_policy")
+ private VolumeQosPolicy qosPolicy = null;
@JsonProperty("size")
private Long size = null;
@JsonProperty("target")
@@ -178,6 +180,15 @@ public String getPath() {
public void setPath(String path) {
this.path = path;
}
+
+ public VolumeQosPolicy getQosPolicy() {
+ return qosPolicy;
+ }
+
+ public void setQosPolicy(VolumeQosPolicy qosPolicy) {
+ this.qosPolicy = qosPolicy;
+ }
+
public Long getSize() {
return size;
}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
index 922751c9a77a..0b2e09dbec45 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java
@@ -83,6 +83,9 @@ public static PropertyClassEnum fromValue(String value) {
@JsonProperty("name")
private String name = null;
+ @JsonProperty("qos_policy")
+ private VolumeQosPolicy qosPolicy = null;
+
@JsonProperty("clone")
private Clone clone = null;
@@ -202,6 +205,14 @@ public void setName(String name) {
this.name = name;
}
+ public VolumeQosPolicy getQosPolicy() {
+ return qosPolicy;
+ }
+
+ public void setQosPolicy(VolumeQosPolicy qosPolicy) {
+ this.qosPolicy = qosPolicy;
+ }
+
public Lun osType(OsTypeEnum osType) {
this.osType = osType;
return this;
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeQosPolicy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeQosPolicy.java
index 7a9a4307ab1a..2e6ce1b6fa3c 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeQosPolicy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeQosPolicy.java
@@ -26,43 +26,23 @@
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class VolumeQosPolicy {
- @JsonProperty("max_throughput_iops")
- private Integer maxThroughputIops = null;
-
- @JsonProperty("max_throughput_mbps")
- private Integer maxThroughputMbps = null;
-
- @JsonProperty("min_throughput_iops")
- private Integer minThroughputIops = null;
-
+ @JsonProperty("fixed")
+ private Fixed fixed;
@JsonProperty("name")
private String name = null;
-
@JsonProperty("uuid")
private String uuid = null;
+ @JsonProperty("svm")
+ private Svm svm;
+ @JsonProperty("object_count")
+ private Integer objectCount;
- public Integer getMaxThroughputIops() {
- return maxThroughputIops;
- }
-
- public void setMaxThroughputIops(Integer maxThroughputIops) {
- this.maxThroughputIops = maxThroughputIops;
- }
-
- public Integer getMaxThroughputMbps() {
- return maxThroughputMbps;
+ public Fixed getFixed() {
+ return fixed;
}
- public void setMaxThroughputMbps(Integer maxThroughputMbps) {
- this.maxThroughputMbps = maxThroughputMbps;
- }
-
- public Integer getMinThroughputIops() {
- return minThroughputIops;
- }
-
- public void setMinThroughputIops(Integer minThroughputIops) {
- this.minThroughputIops = minThroughputIops;
+ public void setFixed(Fixed fixed) {
+ this.fixed = fixed;
}
public String getName() {
@@ -80,4 +60,57 @@ public String getUuid() {
public void setUuid(String uuid) {
this.uuid = uuid;
}
+
+ public Svm getSvm() {
+ return svm;
+ }
+
+ public void setSvm(Svm svm) {
+ this.svm = svm;
+ }
+
+ public Integer getObjectCount() {
+ return objectCount;
+ }
+
+ public void setObjectCount(Integer objectCount) {
+ this.objectCount = objectCount;
+ }
+
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class Fixed {
+ @JsonProperty("capacity_shared")
+ private Boolean capacityShared;
+
+ @JsonProperty("min_throughput_iops")
+ private Long minThroughputIops;
+
+ @JsonProperty("max_throughput_iops")
+ private Long maxThroughputIops;
+
+ public Boolean getCapacityShared() {
+ return capacityShared;
+ }
+
+ public void setCapacityShared(Boolean capacityShared) {
+ this.capacityShared = capacityShared;
+ }
+
+ public Long getMinThroughputIops() {
+ return minThroughputIops;
+ }
+
+ public void setMinThroughputIops(Long minThroughputIops) {
+ this.minThroughputIops = minThroughputIops;
+ }
+
+ public Long getMaxThroughputIops() {
+ return maxThroughputIops;
+ }
+
+ public void setMaxThroughputIops(Long maxThroughputIops) {
+ this.maxThroughputIops = maxThroughputIops;
+ }
+ }
}
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 8e8150e24e2c..55fb7c868c49 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
@@ -57,6 +57,7 @@
import com.cloud.agent.api.StoragePoolInfo;
import com.cloud.alert.AlertManager;
+import com.cloud.capacity.CapacityManager;
import com.cloud.dc.ClusterVO;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.exception.InvalidParameterValueException;
@@ -81,6 +82,7 @@ public class OntapPrimaryDatastoreLifecycle extends BasePrimaryDataStoreLifeCycl
@Inject private PrimaryDataStoreDao storagePoolDao;
@Inject private StoragePoolDetailsDao storagePoolDetailsDao;
@Inject private AlertManager _alertMgr;
+ @Inject private CapacityManager _capacityMgr;
private static final Logger logger = LogManager.getLogger(OntapPrimaryDatastoreLifecycle.class);
private static final long ONTAP_MIN_VOLUME_SIZE_IN_BYTES = 20971520L;
@@ -101,6 +103,7 @@ public DataStore initialize(Map dsInfos) {
String storagePoolName = (String) dsInfos.get("name");
String providerName = (String) dsInfos.get("providerName");
Long capacityBytes = (Long) dsInfos.get("capacityBytes");
+ Long capacityIops = (Long) dsInfos.get("capacityIops");
boolean managed = (boolean) dsInfos.get("managed");
String tags = (String) dsInfos.get("tags");
Boolean isTagARule = (Boolean) dsInfos.get("isTagARule");
@@ -113,7 +116,7 @@ public DataStore initialize(Map dsInfos) {
@SuppressWarnings("unchecked")
Map details = (Map) dsInfos.get("details");
- validateInitializeInputs(capacityBytes, podId, clusterId, zoneId, storagePoolName, providerName, managed, details);
+ validateInitializeInputs(capacityBytes, capacityIops, podId, clusterId, zoneId, storagePoolName, providerName, managed, details);
PrimaryDataStoreParameters parameters = new PrimaryDataStoreParameters();
if (clusterId != null) {
@@ -140,6 +143,7 @@ public DataStore initialize(Map dsInfos) {
StorageStrategy storageStrategy = StorageProviderFactory.getStrategy(ontapStorage);
boolean isValid = storageStrategy.connect();
if (isValid) {
+ details.put(OntapStorageConstants.IS_AFF, Boolean.toString(storageStrategy.isAff()));
if (storageStrategy.getResolvedSvmUuid() != null && !storageStrategy.getResolvedSvmUuid().isEmpty()) {
details.put(OntapStorageConstants.SVM_UUID, storageStrategy.getResolvedSvmUuid());
}
@@ -184,7 +188,7 @@ public DataStore initialize(Map dsInfos) {
logger.info("Setting NFS path for storage pool: " + path + ", port: " + port + " with mount option: vers=3");
break;
case ISCSI:
- parameters.setType(Storage.StoragePoolType.Iscsi);
+ parameters.setType(Storage.StoragePoolType.OntapiSCSI);
path = storageStrategy.getStoragePath();
port = OntapStorageConstants.ISCSI_PORT;
logger.info("Setting iSCSI path for storage pool: " + path + ", port: " + port);
@@ -207,12 +211,13 @@ public DataStore initialize(Map dsInfos) {
parameters.setProviderName(providerName);
parameters.setManaged(managed);
parameters.setCapacityBytes(capacityBytes);
+ parameters.setCapacityIops(capacityIops);
parameters.setUsedBytes(0);
return _dataStoreHelper.createPrimaryDataStore(parameters);
}
- private void validateInitializeInputs(Long capacityBytes, Long podId, Long clusterId, Long zoneId,
+ private void validateInitializeInputs(Long capacityBytes, Long capacityIops, Long podId, Long clusterId, Long zoneId,
String storagePoolName, String providerName, boolean managed, Map details) {
if (capacityBytes == null || capacityBytes <= 0) {
@@ -223,6 +228,10 @@ private void validateInitializeInputs(Long capacityBytes, Long podId, Long clust
throw new InvalidParameterValueException("Storage pool capacity " + capacityBytes + " bytes is below the ONTAP minimum volume size of "
+ ONTAP_MIN_VOLUME_SIZE_IN_BYTES + " bytes (20 MB)");
}
+ // IOPS capacity is optional; when left blank no pool-level IOPS ceiling is enforced.
+ if (capacityIops != null && capacityIops <= 0) {
+ throw new InvalidParameterValueException("Storage pool IOPS capacity must be greater than 0");
+ }
// Validate scope
if (podId == null ^ clusterId == null) {
@@ -551,7 +560,62 @@ public boolean migrateToObjectStore(DataStore store) {
@Override
public void updateStoragePool(StoragePool storagePool, Map details) {
+ String newCapacityIopsStr = details.get(PrimaryDataStoreLifeCycle.CAPACITY_IOPS);
+ if (newCapacityIopsStr != null) {
+ validateUpdatedCapacityIops(storagePool, newCapacityIopsStr);
+ }
+
+ String newCapacityStr = details.get(PrimaryDataStoreLifeCycle.CAPACITY_BYTES);
+ if (newCapacityStr == null) {
+ logger.debug("No capacity change requested for pool: {}, skipping FlexVolume resize", storagePool.getName());
+ return;
+ }
+ long currentCapacityBytes = storagePool.getCapacityBytes();
+ long newCapacityBytes = Long.parseLong(newCapacityStr);
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+
+ String volumeUuid = details.get(OntapStorageConstants.VOLUME_UUID);
+ if (volumeUuid == null || volumeUuid.isEmpty()) {
+ logger.error("Volume UUID or name not found in details for pool: {}, cannot resize", storagePool.getName());
+ throw new CloudRuntimeException("Volume UUID or name not found in details, cannot resize ONTAP FlexVolume");
+ }
+
+ Volume volume = new Volume();
+ volume.setUuid(volumeUuid);
+ volume.setName(details.get(OntapStorageConstants.VOLUME_NAME));
+ volume.setSize(newCapacityBytes);
+ try {
+ storageStrategy.updateStorageVolume(volume);
+ logger.info("Successfully resized ONTAP FlexVolume '{}' (UUID: {}) for pool '{}' from {} bytes to {} bytes",
+ volume.getName(), volume.getUuid(), storagePool.getName(), currentCapacityBytes, newCapacityBytes);
+ } catch (Exception e) {
+ logger.error("Exception while resizing FlexVolume for pool: {}. Error: {}", storagePool.getName(), e.getMessage(), e);
+ throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume for pool: " + storagePool.getName() + ". " + e.getMessage(), e);
+ }
+ }
+
+ private void validateUpdatedCapacityIops(StoragePool storagePool, String newCapacityIopsStr) {
+ long newCapacityIops;
+ try {
+ newCapacityIops = Long.parseLong(newCapacityIopsStr);
+ } catch (NumberFormatException e) {
+ throw new InvalidParameterValueException("Invalid storage pool IOPS capacity: " + newCapacityIopsStr);
+ }
+ if (newCapacityIops <= 0) {
+ throw new InvalidParameterValueException("Storage pool IOPS capacity must be greater than 0");
+ }
+
+ StoragePoolVO storagePoolVO = storagePoolDao.findById(storagePool.getId());
+ if (storagePoolVO == null) {
+ throw new InvalidParameterValueException("Storage pool not found for id: " + storagePool.getId());
+ }
+ long allocatedIops = _capacityMgr.getUsedIops(storagePoolVO);
+ if (newCapacityIops < allocatedIops) {
+ throw new InvalidParameterValueException(String.format(
+ "Cannot set IOPS capacity of storage pool %s to %d IOPS because %d IOPS are already allocated",
+ storagePool.getName(), newCapacityIops, allocatedIops));
+ }
}
@Override
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
index 0a8edd704aed..5ee9f52273e2 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
@@ -19,29 +19,38 @@
package org.apache.cloudstack.storage.service;
-import java.util.ArrayList;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import com.cloud.utils.StringUtils;
import org.apache.cloudstack.storage.feign.FeignClientFactory;
import org.apache.cloudstack.storage.feign.client.AggregateFeignClient;
+import org.apache.cloudstack.storage.feign.client.ClusterFeignClient;
import org.apache.cloudstack.storage.feign.client.JobFeignClient;
import org.apache.cloudstack.storage.feign.client.NASFeignClient;
import org.apache.cloudstack.storage.feign.client.NetworkFeignClient;
+import org.apache.cloudstack.storage.feign.client.QosFeignClient;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
+import org.apache.cloudstack.storage.feign.client.EmsFeignClient;
import org.apache.cloudstack.storage.feign.client.SvmFeignClient;
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.Aggregate;
+import org.apache.cloudstack.storage.feign.model.Cluster;
+import org.apache.cloudstack.storage.feign.model.ClusterNode;
+import org.apache.cloudstack.storage.feign.model.EmsApplicationLog;
import org.apache.cloudstack.storage.feign.model.IpInterface;
import org.apache.cloudstack.storage.feign.model.IscsiService;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Svm;
+import org.apache.cloudstack.storage.feign.model.Version;
import org.apache.cloudstack.storage.feign.model.Volume;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
@@ -74,9 +83,11 @@ public abstract class StorageStrategy {
protected SvmFeignClient svmFeignClient;
protected JobFeignClient jobFeignClient;
protected NetworkFeignClient networkFeignClient;
+ protected QosFeignClient qosFeignClient;
protected SANFeignClient sanFeignClient;
protected NASFeignClient nasFeignClient;
protected SnapshotFeignClient snapshotFeignClient;
+ protected ClusterFeignClient clusterFeignClient;
protected OntapStorage storage;
@@ -105,9 +116,163 @@ public StorageStrategy(OntapStorage ontapStorage) {
this.svmFeignClient = feignClientFactory.createClient(SvmFeignClient.class, baseURL);
this.jobFeignClient = feignClientFactory.createClient(JobFeignClient.class, baseURL);
this.networkFeignClient = feignClientFactory.createClient(NetworkFeignClient.class, baseURL);
+ this.qosFeignClient = feignClientFactory.createClient(QosFeignClient.class, baseURL);
this.sanFeignClient = feignClientFactory.createClient(SANFeignClient.class, baseURL);
this.nasFeignClient = feignClientFactory.createClient(NASFeignClient.class, baseURL);
this.snapshotFeignClient = feignClientFactory.createClient(SnapshotFeignClient.class, baseURL);
+ this.clusterFeignClient = feignClientFactory.createClient(ClusterFeignClient.class, baseURL);
+ }
+
+ /**
+ * Fetches the ONTAP {@link Cluster} (name, uuid, version) and, for ASUP, the rolled-up node
+ * {@code model} and {@code platformType} from {@code GET /api/cluster/nodes}. Best-effort:
+ * returns {@code null} if the cluster GET fails. Node fields are left unset if the nodes GET
+ * fails. Callers must never fail a storage operation because of this.
+ *
+ * @return the ONTAP {@link Cluster}, or {@code null} if it cannot be resolved
+ */
+ public Cluster getClusterInfo() {
+ try {
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ Cluster cluster = clusterFeignClient.getCluster(authHeader, true);
+ if (cluster != null) {
+ populateNodeAsupFields(cluster, authHeader);
+ }
+ return cluster;
+ } catch (Exception e) {
+ logger.warn("getClusterInfo: failed to fetch ONTAP cluster info for storage IP {}: {}",
+ storage.getStorageIP(), e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Sets {@link Cluster#setModel(String)} and {@link Cluster#setPlatformType(String)} from one
+ * {@code GET /api/cluster/nodes}. Distinct models are joined with a comma. Platform type is
+ * {@code performance}, {@code capacity}, {@code fas}, or {@code composite} when personalities mix.
+ */
+ private void populateNodeAsupFields(Cluster cluster, String authHeader) {
+ try {
+ Map query = new HashMap<>();
+ query.put(OntapStorageConstants.FIELDS, OntapStorageConstants.CLUSTER_NODE_ASUP_FIELDS);
+ OntapResponse response = clusterFeignClient.getClusterNodes(authHeader, query);
+ if (response == null || response.getRecords() == null || response.getRecords().isEmpty()) {
+ return;
+ }
+ LinkedHashSet models = new LinkedHashSet<>();
+ LinkedHashSet platformTypes = new LinkedHashSet<>();
+ for (ClusterNode node : response.getRecords()) {
+ if (node == null) {
+ continue;
+ }
+ if (node.getModel() != null && !node.getModel().isBlank()) {
+ models.add(node.getModel().trim());
+ }
+ platformTypes.add(classifyNodePlatformType(node));
+ }
+ if (!models.isEmpty()) {
+ cluster.setModel(String.join(OntapStorageConstants.COMMA, models));
+ }
+ cluster.setPlatformType(rollupPlatformType(platformTypes));
+ } catch (Exception e) {
+ logger.warn("getClusterInfo: failed to fetch ONTAP node hardware for storage IP {}: {}",
+ storage.getStorageIP(), e.getMessage());
+ }
+ }
+
+ /**
+ * Extracts a clean, parser-friendly ONTAP version string from a {@link Cluster}.
+ *
+ * Prefers the compact "generation.major.minor" numeric form (e.g. "9.17.1"), which avoids
+ * the colon/date noise in {@code version.full}. Falls back to the verbose {@code version.full}
+ * banner only when the numeric fields are unavailable.
+ *
+ * @param cluster the cluster (may be {@code null})
+ * @return the ONTAP version string, or {@code null} if it cannot be resolved
+ */
+ public String getClusterVersion(Cluster cluster) {
+ if (cluster == null || cluster.getVersion() == null) {
+ return null;
+ }
+ Version version = cluster.getVersion();
+ if (version.getGeneration() != null && version.getMajor() != null && version.getMinor() != null) {
+ return version.getGeneration() + OntapStorageConstants.DOT + version.getMajor()
+ + OntapStorageConstants.DOT + version.getMinor();
+ }
+ if (version.getFull() != null && !version.getFull().isEmpty()) {
+ return version.getFull();
+ }
+ return null;
+ }
+
+ /**
+ * Classifies one node: not all-flash → {@code fas}; all-flash + capacity → {@code capacity};
+ * otherwise all-flash (including performance-optimized or classic AFF) → {@code performance}.
+ */
+ private static String classifyNodePlatformType(ClusterNode node) {
+ boolean allFlash = Boolean.TRUE.equals(node.getAllFlashOptimized());
+ boolean capacity = Boolean.TRUE.equals(node.getCapacityOptimized());
+ if (!allFlash) {
+ return OntapStorageConstants.ASUP_PLATFORM_TYPE_FAS;
+ }
+ if (capacity) {
+ return OntapStorageConstants.ASUP_PLATFORM_TYPE_CAPACITY;
+ }
+ return OntapStorageConstants.ASUP_PLATFORM_TYPE_PERFORMANCE;
+ }
+
+ private static String rollupPlatformType(LinkedHashSet platformTypes) {
+ if (platformTypes == null || platformTypes.isEmpty()) {
+ return null;
+ }
+ if (platformTypes.size() == 1) {
+ return platformTypes.iterator().next();
+ }
+ return OntapStorageConstants.ASUP_PLATFORM_TYPE_COMPOSITE;
+ }
+
+ /**
+ * True when every cluster node reports {@code is_all_flash_optimized} (AFF, including C-series).
+ * A FAS node, or a node that omits the flag, makes this false. Used for min-throughput QoS support.
+ */
+ public boolean isAff() {
+ Map query = new HashMap<>();
+ query.put(OntapStorageConstants.FIELDS, OntapStorageConstants.CLUSTER_NODE_ASUP_FIELDS);
+ OntapResponse response = clusterFeignClient.getClusterNodes(getAuthHeader(), query);
+ if (response == null || response.getRecords() == null
+ || response.getRecords().stream().allMatch(node -> node == null)) {
+ throw new CloudRuntimeException(
+ "Unable to determine whether the ONTAP cluster is AFF or FAS");
+ }
+ for (ClusterNode node : response.getRecords()) {
+ if (node == null || !Boolean.TRUE.equals(node.getAllFlashOptimized())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Pushes a single ASUP (AutoSupport) EMS application-log message to the ONTAP cluster.
+ *
+ * This is strictly best-effort telemetry: any failure is logged and swallowed so that
+ * it can never affect a storage operation or the periodic scheduler.
+ *
+ * @param message the EMS message to send
+ */
+ public void sendAsupMessage(EmsApplicationLog message) {
+ if (message == null) {
+ return;
+ }
+ try {
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ emsFeignClient.sendEmsApplicationLog(authHeader, message);
+ logger.debug("sendAsupMessage: ASUP EMS message [event-id={}] sent to ONTAP cluster at {}",
+ message.getEventId(), storage.getStorageIP());
+ } catch (Exception e) {
+ logger.error("sendAsupMessage: failed to send ASUP EMS message [event-id={}] to ONTAP cluster at {}: {}",
+ message.getEventId(), storage.getStorageIP(), e.getMessage());
+ }
}
/**
@@ -193,7 +358,6 @@ private void validateAndSelectAggregatesForVolumeCreation(String authHeader, Str
logger.error("No aggregates are assigned to SVM " + svmName);
throw new CloudRuntimeException("No aggregates are assigned to SVM " + svmName);
}
- List eligibleAggregates = new ArrayList<>();
for (Aggregate aggr : aggrs) {
logger.debug("Found aggregate: " + aggr.getName() + " with UUID: " + aggr.getUuid());
Aggregate aggrResp = aggregateFeignClient.getAggregateByUUID(authHeader, aggr.getUuid(),
@@ -213,13 +377,12 @@ private void validateAndSelectAggregatesForVolumeCreation(String authHeader, Str
continue;
}
logger.info("Selected aggregate: " + aggr.getName() + " for volume operations.");
- eligibleAggregates.add(aggrResp);
+ this.aggregates = List.of(aggr);
}
- if (eligibleAggregates.isEmpty()) {
+ if (this.aggregates == null || this.aggregates.isEmpty()) {
logger.error("No suitable aggregates found on SVM " + svmName + " for volume creation.");
throw new CloudRuntimeException("No suitable aggregates found on SVM " + svmName + " for volume creation.");
}
- this.aggregates = eligibleAggregates;
}
// Common methods like create/delete etc., should be here
@@ -389,7 +552,25 @@ public Volume createStorageVolume(String volumeName, Long size) {
* @return the updated Volume object
*/
public Volume updateStorageVolume(Volume volume) {
- return null;
+ logger.info("Resizing ONTAP FlexVolume '{}' (UUID: {}) to {} bytes", volume.getName(), volume.getUuid(), volume.getSize());
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ try {
+ Volume resizeRequest = new Volume();
+ resizeRequest.setSize(volume.getSize());
+ JobResponse jobResponse = volumeFeignClient.updateVolume(authHeader, volume.getUuid(), resizeRequest);
+ pollJobIfPresent(jobResponse, "resize FlexVolume [" + volume.getUuid() + "]",
+ OntapStorageConstants.ONTAP_VOLUME_JOB_MAX_RETRIES, OntapStorageConstants.ONTAP_VOLUME_JOB_POLL_INTERVAL_MS);
+ logger.info("FlexVolume '{}' (UUID: {}) resized successfully to {} bytes", volume.getName(), volume.getUuid(), volume.getSize());
+ } catch (FeignException e) {
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ String msg = String.format("Cannot resize FlexVolume '%s' (UUID: %s): volume not found on ONTAP (404). ", volume.getName(), volume.getUuid());
+ logger.error(msg);
+ throw new CloudRuntimeException(msg, e);
+ }
+ logger.error("Exception while resizing FlexVolume '{}' (UUID: {}): {}", volume.getName(), volume.getUuid(), e.getMessage(), e);
+ throw new CloudRuntimeException("Failed to resize ONTAP FlexVolume: " + e.getMessage(), e);
+ }
+ return volume;
}
/**
@@ -533,10 +714,7 @@ public Pair getNetworkInterface() {
IpInterface fallbackInterface = null;
for (IpInterface iface : response.getRecords()) {
- if (iface == null || !Boolean.TRUE.equals(iface.getEnabled()) || !OntapStorageConstants.LIF_STATE_UP.equals(iface.getState())) {
- continue;
- }
- if (iface.getIp() == null || iface.getIp().getAddress() == null) {
+ if (!Boolean.TRUE.equals(iface.getEnabled()) || !OntapStorageConstants.LIF_STATE_UP.equals(iface.getState())) {
continue;
}
if (!isIPv4Address(iface.getIp().getAddress())) {
@@ -622,16 +800,15 @@ private boolean isIPv4Address(String address) {
* into the mounted FlexVolume.
*
* Returns a {@link CloudStackVolume} so the driver can map it to {@code CreateCmdResult}
- * and update {@code template_spool_ref}. SAN populates {@code lun}. NAS does not pre-create
- * an object on the array and returns {@code null}; the driver treats that as no LUN and
- * records the template uuid as the create path.
+ * and update {@code template_spool_ref}. SAN populates {@code lun}; NAS returns an empty
+ * volume (no LUN / file yet).
*
* @param storagePool CloudStack primary storage pool (one FlexVolume)
* @param templateInfo template being cached
* @param details pool details (SVM, protocol, etc.)
* @param sizeInBytes virtual size for the cache object (required for SAN; ignored for NAS)
- * @return SAN cache identity with {@code lun} set, or {@code null} when nothing is
- * pre-created on the array (NAS)
+ * @return created cache identity, or an empty {@link CloudStackVolume} when nothing is
+ * pre-created on the array
*/
abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateInfo templateInfo,
Map details, long sizeInBytes);
@@ -646,7 +823,7 @@ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool,
* @param cloudstackVolume the CloudStack volume to update
* @return the updated CloudStackVolume object
*/
- abstract CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume);
+ public abstract CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume);
/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
@@ -691,17 +868,6 @@ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool,
*/
abstract public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes);
- /**
- * Deletes a file from a FlexVolume by path.
- *
- * NFS strategies override this. Other strategies fail with the strategy type in the error
- * so callers can invoke it without casting to a concrete NAS implementation.
- */
- public void deleteFileByPath(String flexVolUuid, String filePath) {
- throw new CloudRuntimeException("Deleting a file by path is not supported by storage strategy "
- + getClass().getSimpleName());
- }
-
/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
* it is going to mimic
@@ -822,6 +988,112 @@ public String getAuthHeader() {
return OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
}
+ public VolumeQosPolicy createVolumeQosPolicy(String policyName, Long minIops, Long maxIops) {
+ VolumeQosPolicy policy = buildVolumeQosPolicy(policyName, minIops, maxIops);
+ Svm svm = new Svm();
+ svm.setName(storage.getSvmName());
+ policy.setSvm(svm);
+ try {
+ JobResponse response = qosFeignClient.createPolicy(getAuthHeader(), policy);
+ pollJobIfPresent(response, "create QoS policy [" + policyName + "]");
+ } catch (FeignException e) {
+ if (e.status() != 409) {
+ throw new CloudRuntimeException("Failed to create ONTAP QoS policy [" + policyName + "]: "
+ + e.getMessage(), e);
+ }
+ logger.info("QoS policy [{}] already exists; using the existing policy", policyName);
+ }
+
+ VolumeQosPolicy createdPolicy = getVolumeQosPolicy(policyName);
+ return createdPolicy;
+ }
+
+ public void deleteVolumeQosPolicy(String policyUuid) {
+ if (policyUuid == null || policyUuid.isEmpty()) {
+ return;
+ }
+ VolumeQosPolicy policy = getVolumeQosPolicyByUuid(policyUuid);
+ if (policy == null) {
+ logger.info("QoS policy [{}] not found on ONTAP; skipping delete", policyUuid);
+ return;
+ }
+ if (policy.getObjectCount() != null && policy.getObjectCount() > 0) {
+ logger.info("QoS policy [{}] still has object_count={}; skipping delete",
+ policyUuid, policy.getObjectCount());
+ return;
+ }
+ try {
+ JobResponse response = qosFeignClient.deletePolicy(getAuthHeader(), policyUuid);
+ pollJobIfPresent(response, "delete QoS policy [" + policyUuid + "]");
+ } catch (Exception e) {
+ if ((e instanceof FeignException && ((FeignException) e).status() == 409)
+ || OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ logger.info("QoS policy [{}] was not deleted on ONTAP (already absent or conflict): {}",
+ policyUuid, e.getMessage());
+ return;
+ }
+ throw new CloudRuntimeException("Failed to delete ONTAP QoS policy [" + policyUuid + "]: "
+ + e.getMessage(), e);
+ }
+ }
+
+ private VolumeQosPolicy getVolumeQosPolicyByUuid(String policyUuid) {
+ Map queryParams = new HashMap<>();
+ queryParams.put(OntapStorageConstants.FIELDS, OntapStorageConstants.QOS_POLICY_OBJECT_COUNT_FIELDS);
+ try {
+ VolumeQosPolicy policy = qosFeignClient.getPolicy(getAuthHeader(), policyUuid, queryParams);
+ if (policy == null || StringUtils.isEmpty(policy.getUuid())) {
+ throw new CloudRuntimeException("Failed to fetch ONTAP QoS policy [" + policyUuid
+ + "]: empty response");
+ }
+ return policy;
+ } catch (FeignException e) {
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ return null;
+ }
+ throw new CloudRuntimeException("Failed to fetch ONTAP QoS policy [" + policyUuid + "]: "
+ + e.getMessage(), e);
+ }
+ }
+
+ private VolumeQosPolicy getVolumeQosPolicy(String policyName) {
+ Map queryParams = new HashMap<>();
+ queryParams.put(OntapStorageConstants.NAME, policyName);
+ queryParams.put(OntapStorageConstants.SVM_DOT_NAME, storage.getSvmName());
+ try {
+ OntapResponse response = qosFeignClient.getPolicies(getAuthHeader(), queryParams);
+ if (response == null || response.getRecords() == null || response.getRecords().size() <= 0) {
+ throw new CloudRuntimeException("Unable to get ONTAP QoS policy [" + policyName + "] after creation");
+ }
+ VolumeQosPolicy policy = response.getRecords().get(0);
+ if (policy == null || StringUtils.isEmpty(policy.getUuid())) {
+ throw new CloudRuntimeException("Failed to fetch ONTAP QoS policy [" + policyName
+ + "]: empty response");
+ }
+ return policy;
+ } catch (FeignException e) {
+ throw new CloudRuntimeException("Failed to fetch ONTAP QoS policy [" + policyName + "]: "
+ + e.getMessage(), e);
+ }
+ }
+
+ private VolumeQosPolicy buildVolumeQosPolicy(String policyName, Long minIops, Long maxIops) {
+ VolumeQosPolicy.Fixed fixed = new VolumeQosPolicy.Fixed();
+ fixed.setCapacityShared(false);
+ // ONTAP rejects a policy whose throughput limit is zero, so only unlimited-side values are omitted.
+ if (minIops != null && minIops > 0) {
+ fixed.setMinThroughputIops(minIops);
+ }
+ if (maxIops != null && maxIops > 0) {
+ fixed.setMaxThroughputIops(maxIops);
+ }
+
+ VolumeQosPolicy policy = new VolumeQosPolicy();
+ policy.setName(policyName);
+ policy.setFixed(fixed);
+ return policy;
+ }
+
/**
* Polls an ONTAP async job for successful completion.
*
@@ -983,25 +1255,4 @@ public void deleteFlexVolSnapshotForCloudStackVolume(String flexVolUuid, String
+ e.getMessage(), e);
}
}
-
- /**
- * Polls an ONTAP async job when the API response includes a job reference.
- *
- * When no job is returned (synchronous completion), the operation is treated as
- * successful after HTTP 2xx.
- *
- * @param response ONTAP job response (may be null or without a job)
- * @param operationName label for logging and error messages
- */
- public void pollJobIfPresent(JobResponse response, String operationName) {
- if (response == null || response.getJob() == null || response.getJob().getUuid() == null) {
- logger.debug("pollJobIfPresent: No async job returned for operation [{}], continuing without polling",
- operationName);
- return;
- }
- String jobUuid = response.getJob().getUuid();
- if (!Boolean.TRUE.equals(jobPollForSuccess(jobUuid, 60, 2000))) {
- throw new CloudRuntimeException("ONTAP job [" + jobUuid + "] failed for operation [" + operationName + "]");
- }
- }
}
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 7542337b84be..79db438d5d03 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
@@ -104,8 +104,28 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
logger.error("createCloudStackVolume: " + errMsg);
throw new CloudRuntimeException(errMsg);
}
+ if (cloudstackVolume.getFile() != null && cloudstackVolume.getFile().getQosPolicy() != null) {
+ try {
+ updateCloudStackVolume(cloudstackVolume);
+ } catch (RuntimeException qosError) {
+ logger.error("createCloudStackVolume: QoS attach failed; deleting leftover NFS volume file", qosError);
+ try {
+ Answer cleanup = deleteVolumeOnKVMHost(cloudstackVolume.getVolumeInfo());
+ if (cleanup == null || !cleanup.getResult()) {
+ logger.error("createCloudStackVolume: leftover NFS file may remain after QoS attach failure: {}",
+ cleanup != null ? cleanup.getDetails() : "null answer");
+ }
+ } catch (Exception cleanupError) {
+ logger.error("createCloudStackVolume: failed to delete leftover NFS volume file after QoS attach failure",
+ cleanupError);
+ }
+ throw qosError;
+ }
+ }
return cloudstackVolume;
- }catch (Exception e) {
+ } catch (CloudRuntimeException e) {
+ throw e;
+ } catch (Exception e) {
logger.error("createCloudStackVolume: error occured " + e);
throw new CloudRuntimeException(e);
}
@@ -126,8 +146,23 @@ public CloudStackVolume createTemplateCache(StoragePoolVO storagePool, TemplateI
}
@Override
- CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
- return null;
+ public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null
+ || cloudstackVolume.getFlexVolumeUuid() == null || cloudstackVolume.getFile() == null) {
+ throw new CloudRuntimeException("Invalid NFS volume QoS update request");
+ }
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(cloudstackVolume.getFile().getQosPolicy());
+ String filePath = cloudstackVolume.getVolumeInfo().getUuid();
+ try {
+ nasFeignClient.updateFile(getAuthHeader(), cloudstackVolume.getFlexVolumeUuid(), filePath, fileInfo);
+ } catch (FeignException e) {
+ throw new CloudRuntimeException("Failed to apply QoS policy to NFS volume file: " + e.getMessage(), e);
+ }
+ logger.info("Applied QoS policy [{}] to NFS volume file [{}]",
+ cloudstackVolume.getFile().getQosPolicy() != null
+ ? cloudstackVolume.getFile().getQosPolicy().getName() : null, filePath);
+ return cloudstackVolume;
}
@Override
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 3341ba12e354..c783b2914959 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
@@ -90,12 +90,12 @@ public CloudStackVolume createCloudStackVolume(CloudStackVolume cloudstackVolume
} catch (FeignException e) {
logger.error("FeignException occurred while creating LUN: {}, Status: {}, Exception: {}",
cloudstackVolume.getLun().getName(), e.status(), e.getMessage());
- throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
+ throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage(), e);
} catch (CloudRuntimeException e) {
throw e;
} catch (Exception e) {
logger.error("Exception occurred while creating LUN: {}, Exception: {}", cloudstackVolume.getLun().getName(), e.getMessage());
- throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage());
+ throw new CloudRuntimeException("Failed to create Lun: " + e.getMessage(), e);
}
}
@@ -190,8 +190,25 @@ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, St
}
@Override
- CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
- return null;
+ public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ if (cloudstackVolume == null || cloudstackVolume.getLun() == null
+ || cloudstackVolume.getLun().getUuid() == null) {
+ throw new CloudRuntimeException("Invalid iSCSI volume QoS update request");
+ }
+ Lun lunUpdate = new Lun();
+ lunUpdate.setQosPolicy(cloudstackVolume.getLun().getQosPolicy());
+ try {
+ JobResponse response = sanFeignClient.updateLun(
+ getAuthHeader(), cloudstackVolume.getLun().getUuid(), lunUpdate);
+ pollJobIfPresent(response, "update QoS policy on LUN [" + cloudstackVolume.getLun().getUuid() + "]");
+ } catch (FeignException e) {
+ throw new CloudRuntimeException("Failed to apply QoS policy to LUN: " + e.getMessage(), e);
+ }
+ logger.info("Applied QoS policy [{}] to LUN [{}]",
+ cloudstackVolume.getLun().getQosPolicy() != null
+ ? cloudstackVolume.getLun().getQosPolicy().getName() : null,
+ cloudstackVolume.getLun().getUuid());
+ return cloudstackVolume;
}
@Override
@@ -205,7 +222,8 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
Map queryParams = Map.of("allow_delete_while_mapped", "true");
try {
- sanFeignClient.deleteLun(authHeader, cloudstackVolume.getLun().getUuid(), queryParams);
+ JobResponse response = sanFeignClient.deleteLun(authHeader, cloudstackVolume.getLun().getUuid(), queryParams);
+ pollJobIfPresent(response, "delete Lun [" + cloudstackVolume.getLun().getName() + "]");
} catch (FeignException feignEx) {
if (feignEx.status() == 404) {
logger.warn("deleteCloudStackVolume: Lun {} does not exist (status 404), skipping deletion", cloudstackVolume.getLun().getName());
@@ -301,12 +319,11 @@ public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeI
lunSpace.setSize(sizeInBytes);
Lun patch = new Lun();
patch.setSpace(lunSpace);
- sanFeignClient.updateLun(authHeader, lunUuid, patch);
+ JobResponse response = sanFeignClient.updateLun(authHeader, lunUuid, patch);
+ pollJobIfPresent(response, "resize Lun [" + lunUuid + "]");
logger.debug("resizeCloudStackVolume: Lun {} resized to {} bytes", lunUuid, sizeInBytes);
} catch (FeignException e) {
- logger.error("FeignException occurred while resizing LUN: {}, Status: {}, Exception: {}",
- lunUuid, e.status(), e.getMessage());
- throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage(), e);
} catch (Exception e) {
logger.error("Exception occurred while resizing LUN: {}, Exception: {}", lunUuid, e.getMessage());
throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
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 0c53d0ac053e..7421f9380dd3 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
@@ -107,6 +107,16 @@ public class OntapStorageConstants {
public static final String LUN_DOT_NAME = "lun.name";
public static final String IQN = "iqn";
public static final String LUN_DOT_UUID = "lun.uuid";
+ public static final String QOS_POLICY_UUID = "qosPolicyUuid";
+ public static final String IS_AFF = "isAFF";
+ public static final String MIN_IOPS_NOT_SUPPORTED_ON_FAS =
+ "Minimum IOPS is not supported on FAS/non-AFF ONTAP platforms; only maximum IOPS is supported";
+ public static final String QOS_POLICY_NONE = "none";
+ public static final String QOS_POLICY_NAME_PREFIX = "cs_";
+ public static final String QOS_POLICY_NAME_TO = "to_";
+ public static final String QOS_POLICY_NAME_IOPS = "iops_";
+ public static final String UUID = "uuid";
+ public static final String QOS_POLICY_OBJECT_COUNT_FIELDS = "uuid,name,object_count";
public static final String LOGICAL_UNIT_NUMBER = "logical_unit_number";
public static final String IGROUP_DOT_NAME = "igroup.name";
public static final String IGROUP_DOT_UUID = "igroup.uuid";
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 c0be7a576827..ce7a9b89171a 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
@@ -23,9 +23,10 @@
import com.cloud.host.HostVO;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.storage.DataStoreRole;
+import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.ScopeType;
-import com.cloud.storage.SnapshotVO;
import com.cloud.storage.Storage;
+import com.cloud.storage.SnapshotVO;
import com.cloud.storage.VMTemplateStoragePoolVO;
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
@@ -53,6 +54,8 @@
import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
+import org.apache.cloudstack.storage.service.StorageStrategy;
import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -70,12 +73,12 @@
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import static com.cloud.agent.api.to.DataObjectType.SNAPSHOT;
import static com.cloud.agent.api.to.DataObjectType.TEMPLATE;
import static com.cloud.agent.api.to.DataObjectType.VOLUME;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -83,10 +86,12 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -112,6 +117,12 @@ class OntapPrimaryDatastoreDriverTest {
@Mock
private VolumeDetailsDao volumeDetailsDao;
+ @Mock
+ private SnapshotDetailsDao snapshotDetailsDao;
+
+ @Mock
+ private SnapshotDao snapshotDao;
+
@Mock
private VMTemplatePoolDao vmTemplatePoolDao;
@@ -148,11 +159,6 @@ class OntapPrimaryDatastoreDriverTest {
@Mock
private AsyncCompletionCallback commandCallback;
- @Mock
- private SnapshotDetailsDao snapshotDetailsDao;
- @Mock
- private SnapshotDao snapshotDao;
-
@InjectMocks
private OntapPrimaryDatastoreDriver driver;
@@ -163,6 +169,11 @@ void setUp() {
storagePoolDetails = new HashMap<>();
storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.ISCSI.name());
storagePoolDetails.put(OntapStorageConstants.SVM_NAME, "svm1");
+ storagePoolDetails.put(OntapStorageConstants.IS_AFF, "true");
+ lenient().when(volumeInfo.getName()).thenReturn("test-volume");
+ lenient().when(volumeInfo.getSize()).thenReturn(1073741824L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+ lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
}
@Test
@@ -174,10 +185,10 @@ void testGetCapabilities() {
// so StorageSystemSnapshotStrategy handles snapshot backup to secondary storage
assertEquals(Boolean.TRUE.toString(), capabilities.get("STORAGE_SYSTEM_SNAPSHOT"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_SNAPSHOT"));
+ assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_REVERT_VOLUME_TO_SNAPSHOT"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_VOLUME_FROM_VOLUME"));
assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_CREATE_TEMPLATE_FROM_SNAPSHOT"));
- assertEquals(Boolean.FALSE.toString(), capabilities.get(OntapStorageConstants.CAN_DIRECT_ATTACH_SNAPSHOT));
- assertEquals(Boolean.TRUE.toString(), capabilities.get("CAN_REVERT_VOLUME_TO_SNAPSHOT"));
+ assertEquals(Boolean.FALSE.toString(), capabilities.get("CAN_DIRECT_ATTACH_SNAPSHOT"));
}
@Test
@@ -208,9 +219,10 @@ void testCreateAsync_VolumeWithISCSI_Success() {
when(volumeInfo.getName()).thenReturn("test-volume");
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
- when(storagePool.getName()).thenReturn("vol1");
- when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ lenient().when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
@@ -270,8 +282,6 @@ void testCreateAsync_VolumeWithNFS_Success() {
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
.thenReturn(nasStrategy);
- utilityMock.when(() -> OntapStorageUtils.createCloudStackVolumeRequestByProtocol(
- any(), any(), any())).thenReturn(mockCloudStackVolume);
when(nasStrategy.createCloudStackVolume(any())).thenReturn(mockCloudStackVolume);
@@ -292,7 +302,9 @@ void testCreateAsync_VolumeWithNFS_Success() {
@Test
void testCreateAsync_UnsupportedHypervisor_FailsWithError() {
- storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.ISCSI.name());
+ // Use NFS so LUN OS-type resolution does not fail earlier in getOSTypeFromHypervisor;
+ // the failure under test is image-format resolution for non-KVM hypervisors.
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
when(dataStore.getId()).thenReturn(1L);
when(dataStore.getName()).thenReturn("ontap-pool");
@@ -302,12 +314,16 @@ void testCreateAsync_UnsupportedHypervisor_FailsWithError() {
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.VMware);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ CloudStackVolume mockCloudStackVolume = new CloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ when(nasStrategy.createCloudStackVolume(any())).thenReturn(mockCloudStackVolume);
driver.createAsync(dataStore, volumeInfo, createCallback);
@@ -325,30 +341,17 @@ void testCreateAsync_KvmUnsupportedProtocol_FailsWithError() {
when(dataStore.getId()).thenReturn(1L);
when(dataStore.getName()).thenReturn("ontap-pool");
when(volumeInfo.getType()).thenReturn(VOLUME);
- when(volumeInfo.getId()).thenReturn(100L);
when(volumeInfo.getName()).thenReturn("test-volume");
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
-
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- when(storagePool.getId()).thenReturn(1L);
- when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
-
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
- driver.createAsync(dataStore, volumeInfo, createCallback);
+ driver.createAsync(dataStore, volumeInfo, createCallback);
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
- verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- assertTrue(resultCaptor.getValue().getResult().contains("Unsupported protocol FC"));
- assertTrue(resultCaptor.getValue().getResult().contains("No enum constant"));
- }
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains("Unsupported protocol [FC]"));
}
@Test
@@ -378,6 +381,7 @@ void testDeleteAsync_ISCSIVolume_Success() {
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
@@ -396,6 +400,8 @@ void testDeleteAsync_ISCSIVolume_Success() {
assertNotNull(result);
assertTrue(result.isSuccess());
verify(sanStrategy).deleteCloudStackVolume(any(CloudStackVolume.class));
+ verify(volumeDao, never()).remove(anyLong());
+ verify(volumeDetailsDao, never()).removeDetails(anyLong());
}
}
@@ -568,6 +574,7 @@ void testGrantAccess_ClusterScope_Success() {
verify(sanStrategy).getAccessGroup(any());
verify(sanStrategy).ensureLunMapped(anyString(), anyString(), anyString());
verify(sanStrategy, never()).validateInitiatorInAccessGroup(anyString(), anyString(), any(Igroup.class));
+ verify(volumeInfo, never()).getVolume();
}
}
@@ -950,6 +957,7 @@ void testCreateAsync_VolumeClonedFromTemplate_ClonesWithoutGrowing() {
assertEquals("template-lun-uuid", requestCaptor.getValue().getLun().getClone().getSource().getUuid());
verify(sanStrategy, never()).createCloudStackVolume(any());
verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), eq("cloned-lun-uuid"), eq(false));
}
}
@@ -974,49 +982,6 @@ void testCreateAsync_VolumeClonedFromTemplate_GrowsWhenOfferingIsLarger() {
}
}
- @Test
- void testCreateAsync_VolumeClonedFromTemplate_FailsWhenTemplateSizeIsInvalid() {
- stubVolumeCloneFromTemplate(0L, 21474836480L);
-
- try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
-
- driver.createAsync(dataStore, volumeInfo, createCallback);
-
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
- verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- assertTrue(resultCaptor.getValue().getResult().contains("Template cache size is missing or invalid"));
- verify(sanStrategy, never()).cloneCloudStackVolume(any());
- verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
- }
- }
-
- @Test
- void testCreateAsync_Volume_InvalidCloneOfTemplateDetail() {
- when(dataStore.getId()).thenReturn(1L);
- when(dataStore.getName()).thenReturn("ontap-pool");
- when(volumeInfo.getType()).thenReturn(VOLUME);
- when(volumeInfo.getId()).thenReturn(100L);
- when(volumeInfo.getName()).thenReturn("test-volume");
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
- .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "not-a-number", false));
-
- driver.createAsync(dataStore, volumeInfo, createCallback);
-
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
- verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- assertTrue(resultCaptor.getValue().getResult().contains("not-a-number"));
- assertTrue(resultCaptor.getValue().getResult().contains("100"));
- verify(sanStrategy, never()).cloneCloudStackVolume(any());
- verify(nasStrategy, never()).cloneCloudStackVolume(any());
- }
-
@Test
void testCreateAsync_VolumeClonedFromSnapshot_IscsiSuccessWithoutGrow() {
stubVolumeCloneFromSnapshot(5368709120L, 5368709120L, ProtocolType.ISCSI.name());
@@ -1314,7 +1279,7 @@ private void stubVolumeCloneFromSnapshot(long snapshotSize, long volumeSize, Str
// getId is only used after snapshot_details validation succeeds; lenient for early-fail tests.
lenient().when(storagePool.getId()).thenReturn(1L);
lenient().when(storagePool.getName()).thenReturn("vol1");
- lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
@@ -1358,13 +1323,16 @@ private void stubVolumeCloneFromTemplate(long templateSize, long volumeSize) {
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
lenient().when(storagePool.getName()).thenReturn("vol1");
- lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ lenient().when(storagePool.getCapacityIops()).thenReturn(null);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
lenient().when(volumeVO.getId()).thenReturn(100L);
- lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
+ // createAsync checks cloneOfSnapshot before cloneOfTemplate; under STRICT_STUBS an
+ // unstubbed alternate key on the same method is treated as an argument mismatch.
+ lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
@@ -1519,6 +1487,74 @@ void testCreateAsync_VolumeClonedFromTemplateNFS_ClonesFile() {
assertEquals("template-uuid", requestCaptor.getValue().getFile().getPath());
assertEquals("volume-uuid", requestCaptor.getValue().getDestinationPath());
verify(nasStrategy, never()).resizeCloudStackVolume(any(), anyLong());
+ verify(nasStrategy, never()).updateCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplate_RootWithIops_SetsQosOnLunCreate() {
+ stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+
+ Lun clonedLun = new Lun();
+ clonedLun.setName("/vol/vol1/test_volume");
+ clonedLun.setUuid("cloned-lun-uuid");
+ CloudStackVolume cloned = new CloudStackVolume();
+ cloned.setLun(clonedLun);
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-root-uuid", "cs_100_to_200_iops_svm1");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloned, qosPolicy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(sanStrategy).cloneCloudStackVolume(argThat(request ->
+ request.getLun() != null && request.getLun().getQosPolicy() != null
+ && "qos-root-uuid".equals(request.getLun().getQosPolicy().getUuid())));
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID),
+ eq("qos-root-uuid"), eq(false));
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplateNFS_RootWithIops_AttachesQosToFile() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid");
+ stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(volumeInfo.getUuid()).thenReturn("volume-uuid");
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+ when(templatePoolRef.getInstallPath()).thenReturn("template-uuid");
+
+ CloudStackVolume cloned = new CloudStackVolume();
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-nfs-root-uuid", "cs_100_to_200_iops_svm1");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, nasStrategy, cloned, qosPolicy);
+ when(nasStrategy.cloneCloudStackVolume(any())).thenReturn(cloned);
+ when(nasStrategy.updateCloudStackVolume(any())).thenReturn(cloned);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(nasStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(nasStrategy).updateCloudStackVolume(argThat(request ->
+ request.getFile() != null && request.getFile().getQosPolicy() != null
+ && "qos-nfs-root-uuid".equals(request.getFile().getQosPolicy().getUuid())));
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID),
+ eq("qos-nfs-root-uuid"), eq(false));
}
}
@@ -1624,141 +1660,928 @@ void testRevokeAccess_Template_UnmapsCacheLun() {
}
@Test
- void testCanHostAccessStoragePool_ReturnsTrue() {
- assertTrue(driver.canHostAccessStoragePool(host, storagePool));
- }
+ void testGetUsedIops_SumsMinIopsIncludingCreatingVolumes() {
+ VolumeVO readyVolume = mock(VolumeVO.class);
+ VolumeVO creatingVolume = mock(VolumeVO.class);
+ VolumeVO volumeWithoutMinIops = mock(VolumeVO.class);
- @Test
- void testIsVmInfoNeeded_ReturnsTrue() {
- assertTrue(driver.isVmInfoNeeded());
- }
+ when(storagePool.getId()).thenReturn(1L);
+ when(readyVolume.getMinIops()).thenReturn(700L);
+ when(creatingVolume.getMinIops()).thenReturn(300L);
+ when(volumeWithoutMinIops.getMinIops()).thenReturn(null);
+ when(volumeDao.findNonDestroyedVolumesByPoolId(1L, null))
+ .thenReturn(List.of(readyVolume, creatingVolume, volumeWithoutMinIops));
- @Test
- void testIsStorageSupportHA_ReturnsTrue() {
- assertTrue(driver.isStorageSupportHA(Storage.StoragePoolType.NetworkFilesystem));
+ assertEquals(1000L, driver.getUsedIops(storagePool));
}
@Test
- void testGetChapInfo_ReturnsNull() {
- assertNull(driver.getChapInfo(volumeInfo));
- }
+ void testResize_IopsUpdate_AttachesQosPolicy() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 0L, 5000L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getCapacityIops()).thenReturn(null);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
- @Test
- void testCanProvideStorageStats_ReturnsFalse() {
- assertFalse(driver.canProvideStorageStats());
- }
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_0_to_5000_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
- @Test
- void testCanProvideVolumeStats_ReturnsFalse() {
- assertFalse(driver.canProvideVolumeStats());
- }
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+ when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
- @Test
- void testGetBytesRequiredForTemplate_NullArgs_ReturnsZero() {
- assertEquals(0L, driver.getBytesRequiredForTemplate(null, storagePool));
- assertEquals(0L, driver.getBytesRequiredForTemplate(templateInfo, null));
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).updateCloudStackVolume(argThat(request ->
+ request.getLun() != null && "lun-uuid-123".equals(request.getLun().getUuid())));
+ verify(volumeDetailsDao).addDetail(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+ }
}
@Test
- void testCreateAsync_Template_StrategyThrows_Fails() {
- when(dataStore.getId()).thenReturn(1L);
- when(dataStore.getName()).thenReturn("ontap-pool");
- when(templateInfo.getType()).thenReturn(TEMPLATE);
- when(templateInfo.getId()).thenReturn(50L);
- when(templateInfo.getSize()).thenReturn(5368709120L);
- when(templateInfo.getName()).thenReturn("tmpl");
-
+ void testResize_IopsUpdate_NfsAttachesQosPolicy() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid");
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 100L, 200L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getCapacityIops()).thenReturn(null);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = nfsCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
- when(sanStrategy.createTemplateCache(any(), any(), any(), anyLong()))
- .thenThrow(new RuntimeException("unexpected"));
+ stubQosCreateMocks(utilityMock, nasStrategy, cloudStackVolume, qosPolicy);
+ when(nasStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
- driver.createAsync(dataStore, templateInfo, createCallback);
+ driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(nasStrategy).updateCloudStackVolume(any());
+ verify(volumeDetailsDao).addDetail(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
}
}
@Test
- void testCreateAsync_VolumeClonedFromTemplate_NullCloneResult_Fails() {
- stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+ void testResize_SameQosPolicy_SkipsAttach() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 0L, 3333L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getCapacityIops()).thenReturn(null);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_0_to_3333_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
- when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(null);
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
- driver.createAsync(dataStore, volumeInfo, createCallback);
+ driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
}
}
@Test
- void testCreateAsync_VolumeClonedFromTemplateNFS_MissingInstallPath_Fails() {
- storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
- stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
- when(templatePoolRef.getInstallPath()).thenReturn(null);
+ void testResize_ClearIops_DetachesQosPolicy() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 0L, 0L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getCapacityIops()).thenReturn(null);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
- driver.createAsync(dataStore, volumeInfo, createCallback);
+ driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
- assertFalse(resultCaptor.getValue().isSuccess());
- verify(nasStrategy, never()).cloneCloudStackVolume(any());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).updateCloudStackVolume(argThat(request ->
+ request.getLun() != null && request.getLun().getQosPolicy() != null
+ && OntapStorageConstants.QOS_POLICY_NONE.equals(request.getLun().getQosPolicy().getName())));
+ verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
}
}
@Test
- void testDeleteAsync_Template_NoSpoolRef_Succeeds() {
- when(dataStore.getId()).thenReturn(1L);
- when(templateInfo.getType()).thenReturn(TEMPLATE);
- when(templateInfo.getId()).thenReturn(50L);
-
+ void testResize_SwitchIops_OldPolicyDeleteFails_StillSucceeds() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 0L, 5000L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getCapacityIops()).thenReturn(null);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "old-qos-uuid", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("new-qos-uuid", "cs_0_to_5000_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+ when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
+ doThrow(new CloudRuntimeException("ONTAP QoS delete failed")).when(sanStrategy)
+ .deleteVolumeQosPolicy("old-qos-uuid");
- driver.deleteAsync(dataStore, templateInfo, commandCallback);
+ driver.resize(volumeInfo, createCallback);
- ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
- verify(commandCallback).complete(resultCaptor.capture());
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- verify(sanStrategy, never()).deleteCloudStackVolume(any());
+ verify(volumeDetailsDao).addDetail(100L, OntapStorageConstants.QOS_POLICY_UUID, "new-qos-uuid", false);
+ verify(sanStrategy).deleteVolumeQosPolicy("old-qos-uuid");
+ verify(sanStrategy, never()).deleteVolumeQosPolicy("new-qos-uuid");
}
}
@Test
- void testGrantAccess_Template_CreatesIgroupWhenMissing() {
- PrimaryDataStore primaryDataStore = mock(PrimaryDataStore.class);
- Map dataStoreDetails = new HashMap<>();
- dataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET, "stale-value");
-
- when(primaryDataStore.getId()).thenReturn(1L);
- when(primaryDataStore.getDetails()).thenReturn(dataStoreDetails);
- when(templateInfo.getType()).thenReturn(TEMPLATE);
- when(templateInfo.getId()).thenReturn(50L);
-
+ void testResize_ClearIops_OldPolicyDeleteFails_StillSucceeds() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 0L, 0L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
when(storagePoolDao.findById(1L)).thenReturn(storagePool);
when(storagePool.getId()).thenReturn(1L);
- when(storagePool.getName()).thenReturn("vol1");
- when(storagePool.getScope()).thenReturn(ScopeType.CLUSTER);
- when(storagePool.getPath()).thenReturn("iqn.1992-08.com.netapp:sn.123456");
+ when(storagePool.getCapacityIops()).thenReturn(null);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
+ doThrow(new CloudRuntimeException("ONTAP QoS delete failed")).when(sanStrategy)
+ .deleteVolumeQosPolicy("qos-uuid");
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
+ }
+ }
+
+ @Test
+ void testResize_MinIopsBeyondPoolCapacity_Fails() {
+ long currentSize = 4L * 1024 * 1024 * 1024;
+ VolumeVO otherVolume = mock(VolumeVO.class);
+ when(otherVolume.getId()).thenReturn(50L);
+ when(otherVolume.getMinIops()).thenReturn(800L);
+
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getpayload()).thenReturn(
+ new ResizeVolumePayload(currentSize, 300L, 1000L, null, false, null, null, true));
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeVO.getPoolId()).thenReturn(1L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("ontap-pool");
+ when(storagePool.getCapacityIops()).thenReturn(1000L);
+ when(volumeDao.findNonDestroyedVolumesByPoolId(1L, null)).thenReturn(List.of(otherVolume, volumeVO));
+
+ driver.resize(volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(
+ "requested total of 1100 IOPS exceeds the pool IOPS capacity of 1000"));
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
+ }
+
+ @Test
+ void testCreateAsync_MinIopsBeyondPoolCapacity_FailsWithCapacityDetails() {
+ VolumeVO allocatedVolume = mock(VolumeVO.class);
+ when(allocatedVolume.getId()).thenReturn(50L);
+ when(allocatedVolume.getMinIops()).thenReturn(800L);
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(volumeInfo.getMinIops()).thenReturn(300L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("ontap-pool");
+ when(storagePool.getCapacityIops()).thenReturn(1000L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDao.findNonDestroyedVolumesByPoolId(1L, null)).thenReturn(List.of(allocatedVolume));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(
+ "storage pool ontap-pool: requested total of 1100 IOPS exceeds the pool IOPS capacity of 1000"));
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ }
+
+ @Test
+ void testCreateAsync_OtherCreatingVolumeCountsAgainstCapacity() {
+ VolumeVO readyVolume = mock(VolumeVO.class);
+ when(readyVolume.getId()).thenReturn(50L);
+ when(readyVolume.getMinIops()).thenReturn(500L);
+
+ VolumeVO otherCreatingVolume = mock(VolumeVO.class);
+ when(otherCreatingVolume.getId()).thenReturn(60L);
+ when(otherCreatingVolume.getMinIops()).thenReturn(300L);
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(volumeInfo.getMinIops()).thenReturn(300L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("ontap-pool");
+ when(storagePool.getCapacityIops()).thenReturn(1000L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDao.findNonDestroyedVolumesByPoolId(1L, null))
+ .thenReturn(List.of(readyVolume, otherCreatingVolume));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(
+ "storage pool ontap-pool: requested total of 1100 IOPS exceeds the pool IOPS capacity of 1000"));
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ }
+
+ @Test
+ void testCreateAsync_DoesNotDoubleCountSelfWhenAlreadyCreating() {
+ VolumeVO readyVolume = mock(VolumeVO.class);
+ when(readyVolume.getId()).thenReturn(50L);
+ when(readyVolume.getMinIops()).thenReturn(500L);
+
+ VolumeVO selfCreatingVolume = mock(VolumeVO.class);
+ when(selfCreatingVolume.getId()).thenReturn(100L);
+
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(volumeInfo.getMinIops()).thenReturn(300L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getName()).thenReturn("vol1");
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
+ when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ when(storagePool.getCapacityIops()).thenReturn(1000L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+ when(volumeDao.findNonDestroyedVolumesByPoolId(1L, null))
+ .thenReturn(List.of(readyVolume, selfCreatingVolume));
+
+ Lun mockLun = new Lun();
+ mockLun.setName("/vol/vol1/lun1");
+ mockLun.setUuid("lun-uuid-123");
+ CloudStackVolume cloudStackVolume = new CloudStackVolume();
+ cloudStackVolume.setLun(mockLun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.createCloudStackVolume(any())).thenReturn(cloudStackVolume);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).createCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_DataDiskFixedIops_CreatesAndPersistsQosPolicy() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(sanStrategy).createCloudStackVolume(argThat(request ->
+ request.getLun() != null && request.getLun().getQosPolicy() != null
+ && "qos-uuid".equals(request.getLun().getQosPolicy().getUuid())));
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID),
+ eq("qos-uuid"), eq(false));
+ verify(sanStrategy, never()).isAff();
+ }
+ }
+
+ @Test
+ void testCreateAsync_FasPoolWithMinIops_FailsWithoutCallingOntap() {
+ stubIscsiVolumeCreate();
+ storagePoolDetails.put(OntapStorageConstants.IS_AFF, "false");
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(OntapStorageConstants.MIN_IOPS_NOT_SUPPORTED_ON_FAS));
+ verify(sanStrategy, never()).isAff();
+ verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_FasPoolWithMaxIopsOnly_CreatesQosPolicyWithoutNodeCall() {
+ stubIscsiVolumeCreate();
+ storagePoolDetails.put(OntapStorageConstants.IS_AFF, "false");
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(0L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_0_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).isAff();
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_0_to_200_iops_svm1"), eq(0L), eq(200L));
+ }
+ }
+
+ @Test
+ void testCreateAsync_MissingAffDetail_FetchesFromNodesAndPersists() {
+ stubIscsiVolumeCreate();
+ storagePoolDetails.remove(OntapStorageConstants.IS_AFF);
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+ when(sanStrategy.isAff()).thenReturn(true);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).isAff();
+ verify(storagePoolDetailsDao).addDetail(1L, OntapStorageConstants.IS_AFF, "true", false);
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ }
+ }
+
+ @Test
+ void testCreateAsync_MissingAffDetailOnFas_PersistsFalseAndRejectsMinIops() {
+ stubIscsiVolumeCreate();
+ storagePoolDetails.remove(OntapStorageConstants.IS_AFF);
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+ when(sanStrategy.isAff()).thenReturn(false);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(OntapStorageConstants.MIN_IOPS_NOT_SUPPORTED_ON_FAS));
+ verify(sanStrategy).isAff();
+ verify(storagePoolDetailsDao).addDetail(1L, OntapStorageConstants.IS_AFF, "false", false);
+ verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_MissingAffDetail_IsAffThrows_StillCreatesWithMinIops() {
+ stubIscsiVolumeCreate();
+ storagePoolDetails.remove(OntapStorageConstants.IS_AFF);
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+ when(sanStrategy.isAff()).thenThrow(new CloudRuntimeException(
+ "Unable to determine whether the ONTAP cluster is AFF or FAS"));
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).isAff();
+ verify(storagePoolDetailsDao, never()).addDetail(eq(1L), eq(OntapStorageConstants.IS_AFF),
+ anyString(), eq(false));
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(sanStrategy).createCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_RootDiskCustomIops_CreatesAndPersistsQosPolicy() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.ROOT);
+ when(volumeInfo.getMinIops()).thenReturn(111L);
+ when(volumeInfo.getMaxIops()).thenReturn(999L);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-root-uuid", "cs_111_to_999_iops_svm1");
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).createVolumeQosPolicy(eq("cs_111_to_999_iops_svm1"), eq(111L), eq(999L));
+ verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID),
+ eq("qos-root-uuid"), eq(false));
+ }
+ }
+
+ @Test
+ void testCreateAsync_NfsDataDiskWithIops_CreatesQosPolicy() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ lenient().when(storagePool.getCapacityIops()).thenReturn(null);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeVO.getId()).thenReturn(100L);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-nfs-uuid", "cs_100_to_200_iops_svm1");
+ CloudStackVolume cloudStackVolume = new CloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ stubQosCreateMocks(utilityMock, nasStrategy, cloudStackVolume, qosPolicy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(nasStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(nasStrategy).createCloudStackVolume(argThat(request ->
+ request.getFile() != null && request.getFile().getQosPolicy() != null
+ && "qos-nfs-uuid".equals(request.getFile().getQosPolicy().getUuid())));
+ }
+ }
+
+ @Test
+ void testCreateAsync_MinIopsGreaterThanMaxIops_Fails() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(200L);
+ when(volumeInfo.getMaxIops()).thenReturn(100L);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ assertTrue(resultCaptor.getValue().getResult().contains(
+ "Minimum IOPS cannot be greater than maximum IOPS"));
+ verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any());
+ verify(sanStrategy, never()).createCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_NoIops_DoesNotCreateQosPolicy() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.createCloudStackVolume(any())).thenReturn(cloudStackVolume);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any());
+ verify(sanStrategy).createCloudStackVolume(argThat(request ->
+ request.getLun() != null && request.getLun().getQosPolicy() == null));
+ }
+ }
+
+ @Test
+ void testCreateAsync_SwapVolumeWithIops_DoesNotCreateQosPolicy() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.SWAP);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.createCloudStackVolume(any())).thenReturn(cloudStackVolume);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ verify(sanStrategy, never()).createVolumeQosPolicy(any(), any(), any());
+ }
+ }
+
+ @Test
+ void testCreateAsync_QosCreateThenLunCreateFails_DeletesUnusedPolicy() {
+ stubIscsiVolumeCreate();
+ when(volumeInfo.getVolumeType()).thenReturn(Volume.Type.DATADISK);
+ when(volumeInfo.getMinIops()).thenReturn(100L);
+ when(volumeInfo.getMaxIops()).thenReturn(200L);
+
+ VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(sanStrategy);
+ when(sanStrategy.createVolumeQosPolicy(nullable(String.class), nullable(Long.class), nullable(Long.class)))
+ .thenReturn(qosPolicy);
+ when(sanStrategy.createCloudStackVolume(any())).thenThrow(new CloudRuntimeException(
+ "Failed to create Lun: {\"error\":{\"code\":\"8454269\"}}"));
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
+ }
+ }
+
+ @Test
+ void testDeleteAsync_DeletesUnusedQosPolicy() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ VolumeDetailVO lunNameDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/lun1", false);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ doNothing().when(sanStrategy).deleteCloudStackVolume(any());
+
+ driver.deleteAsync(dataStore, volumeInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(volumeDetailsDao, never()).removeDetail(100L, OntapStorageConstants.QOS_POLICY_UUID);
+ verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
+ }
+ }
+
+ @Test
+ void testDeleteAsync_QosPolicyDeleteFails_VolumeDeleteStillSucceeds() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ VolumeDetailVO lunNameDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/lun1", false);
+ VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
+ VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
+
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_NAME)).thenReturn(lunNameDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails))
+ .thenReturn(sanStrategy);
+ doNothing().when(sanStrategy).deleteCloudStackVolume(any());
+ doThrow(new CloudRuntimeException("ONTAP QoS delete failed")).when(sanStrategy)
+ .deleteVolumeQosPolicy("qos-uuid");
+
+ driver.deleteAsync(dataStore, volumeInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(volumeDetailsDao, never()).removeDetail(100L, OntapStorageConstants.QOS_POLICY_UUID);
+ verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
+ }
+ }
+
+ private void stubIscsiVolumeCreate() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(volumeInfo.getType()).thenReturn(VOLUME);
+ when(volumeInfo.getId()).thenReturn(100L);
+ when(volumeInfo.getName()).thenReturn("test-volume");
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ lenient().when(storagePool.getId()).thenReturn(1L);
+ lenient().when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
+ lenient().when(storagePool.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM);
+ lenient().when(storagePool.getCapacityIops()).thenReturn(null);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ lenient().when(volumeVO.getId()).thenReturn(100L);
+ }
+
+ private CloudStackVolume iscsiCloudStackVolume() {
+ Lun mockLun = new Lun();
+ mockLun.setName("/vol/vol1/lun1");
+ mockLun.setUuid("lun-uuid-123");
+ CloudStackVolume volume = new CloudStackVolume();
+ volume.setLun(mockLun);
+ return volume;
+ }
+
+ private CloudStackVolume nfsCloudStackVolume() {
+ FileInfo fileInfo = new FileInfo();
+ CloudStackVolume volume = new CloudStackVolume();
+ volume.setFlexVolumeUuid("flex-uuid");
+ volume.setFile(fileInfo);
+ volume.setVolumeInfo(volumeInfo);
+ return volume;
+ }
+
+ private VolumeQosPolicy qosPolicy(String uuid, String name) {
+ VolumeQosPolicy policy = new VolumeQosPolicy();
+ policy.setUuid(uuid);
+ policy.setName(name);
+ return policy;
+ }
+
+ private void stubQosCreateMocks(MockedStatic utilityMock,
+ StorageStrategy strategy,
+ CloudStackVolume cloudStackVolume, VolumeQosPolicy qosPolicy) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(strategy);
+ when(strategy.createVolumeQosPolicy(nullable(String.class), nullable(Long.class), nullable(Long.class)))
+ .thenReturn(qosPolicy);
+ lenient().when(strategy.createCloudStackVolume(any())).thenReturn(cloudStackVolume);
+ }
+
+ @Test
+ void testCanHostAccessStoragePool_ReturnsTrue() {
+ assertTrue(driver.canHostAccessStoragePool(host, storagePool));
+ }
+
+ @Test
+ void testIsVmInfoNeeded_ReturnsTrue() {
+ assertTrue(driver.isVmInfoNeeded());
+ }
+
+ @Test
+ void testIsStorageSupportHA_ReturnsTrue() {
+ assertTrue(driver.isStorageSupportHA(Storage.StoragePoolType.NetworkFilesystem));
+ }
+
+ @Test
+ void testGetChapInfo_ReturnsNull() {
+ assertNull(driver.getChapInfo(volumeInfo));
+ }
+
+ @Test
+ void testCanProvideStorageStats_ReturnsFalse() {
+ assertFalse(driver.canProvideStorageStats());
+ }
+
+ @Test
+ void testCanProvideVolumeStats_ReturnsFalse() {
+ assertFalse(driver.canProvideVolumeStats());
+ }
+
+ @Test
+ void testGetBytesRequiredForTemplate_NullArgs_ReturnsZero() {
+ assertEquals(0L, driver.getBytesRequiredForTemplate(null, storagePool));
+ assertEquals(0L, driver.getBytesRequiredForTemplate(templateInfo, null));
+ }
+
+ @Test
+ void testCreateAsync_Template_StrategyThrows_Fails() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(dataStore.getName()).thenReturn("ontap-pool");
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+ when(templateInfo.getSize()).thenReturn(5368709120L);
+ when(templateInfo.getName()).thenReturn("tmpl");
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.createTemplateCache(any(), any(), any(), anyLong()))
+ .thenThrow(new RuntimeException("unexpected"));
+
+ driver.createAsync(dataStore, templateInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplate_NullCloneResult_Fails() {
+ stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+ when(sanStrategy.cloneCloudStackVolume(any())).thenReturn(null);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ }
+ }
+
+ @Test
+ void testCreateAsync_VolumeClonedFromTemplateNFS_MissingInstallPath_Fails() {
+ storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
+ stubVolumeCloneFromTemplate(5368709120L, 5368709120L);
+ when(templatePoolRef.getInstallPath()).thenReturn(null);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(nasStrategy);
+
+ driver.createAsync(dataStore, volumeInfo, createCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
+ verify(createCallback).complete(resultCaptor.capture());
+ assertFalse(resultCaptor.getValue().isSuccess());
+ verify(nasStrategy, never()).cloneCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testDeleteAsync_Template_NoSpoolRef_Succeeds() {
+ when(dataStore.getId()).thenReturn(1L);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any())).thenReturn(sanStrategy);
+
+ driver.deleteAsync(dataStore, templateInfo, commandCallback);
+
+ ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class);
+ verify(commandCallback).complete(resultCaptor.capture());
+ assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy, never()).deleteCloudStackVolume(any());
+ }
+ }
+
+ @Test
+ void testGrantAccess_Template_CreatesIgroupWhenMissing() {
+ PrimaryDataStore primaryDataStore = mock(PrimaryDataStore.class);
+ Map dataStoreDetails = new HashMap<>();
+ dataStoreDetails.put(PrimaryDataStore.MANAGED_STORE_TARGET, "stale-value");
+
+ when(primaryDataStore.getId()).thenReturn(1L);
+ when(primaryDataStore.getDetails()).thenReturn(dataStoreDetails);
+ when(templateInfo.getType()).thenReturn(TEMPLATE);
+ when(templateInfo.getId()).thenReturn(50L);
+
+ when(storagePoolDao.findById(1L)).thenReturn(storagePool);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("vol1");
+ when(storagePool.getScope()).thenReturn(ScopeType.CLUSTER);
+ when(storagePool.getPath()).thenReturn("iqn.1992-08.com.netapp:sn.123456");
+ when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
+ when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(templatePoolRef);
when(templatePoolRef.getId()).thenReturn(7L);
HostVO hostVo = mock(HostVO.class);
@@ -1797,7 +2620,7 @@ void testCreateAsync_VolumeClonedFromTemplate_MissingSpoolRef_Fails() {
when(storagePool.getId()).thenReturn(1L);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(volumeVO);
- lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
+ lenient().when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_SNAPSHOT)).thenReturn(null);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.CLONE_OF_TEMPLATE))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.CLONE_OF_TEMPLATE, "50", false));
when(vmTemplatePoolDao.findByPoolTemplate(1L, 50L, null)).thenReturn(null);
@@ -1813,6 +2636,7 @@ void testCreateAsync_VolumeClonedFromTemplate_MissingSpoolRef_Fails() {
verify(sanStrategy, never()).cloneCloudStackVolume(any());
}
}
+
private static VolumeVO temporarySnapshotCopyVolume() {
return new VolumeVO(Volume.Type.DATADISK, "ROOT-5_20260924.TMP", 1L, 1L, 2L, 0L,
Storage.ProvisioningType.THIN, 5368709120L, 0L, 0L, "");
@@ -1955,7 +2779,7 @@ void testCopyAsync_IscsiTemporarySnapshotCopy_ClonesLunAndRecordsIdentity() {
SnapshotInfo snapshotInfo = stubSnapshotToVolumeCopy(dbVolume);
when(volumeInfo.getVolume()).thenReturn(inMemoryVolume);
stubSnapshotCloneSource(ProtocolType.ISCSI.name());
- when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
Lun clonedLun = new Lun();
clonedLun.setName("/vol/vol1/cs_tmp_snap_200_100");
@@ -2071,6 +2895,7 @@ void testDeleteAsync_TemporarySnapshotCopy_NullCallbackRemovesRecord() {
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_NAME, "/vol/vol1/cs_tmp_snap_200_100", false));
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
.thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "tmp-lun-uuid", false));
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)).thenReturn(sanStrategy);
@@ -2102,7 +2927,7 @@ void testGrantAccess_TemporarySnapshotCopy_SyncsIscsiPathOnInMemoryVolume() {
when(storagePool.getId()).thenReturn(1L);
when(storagePool.getScope()).thenReturn(ScopeType.CLUSTER);
when(storagePool.getPath()).thenReturn("iqn.1992-08.com.netapp:sn.123456");
- when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.Iscsi);
+ when(storagePool.getPoolType()).thenReturn(Storage.StoragePoolType.OntapiSCSI);
when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDao.findById(100L)).thenReturn(dbVolume);
when(host.getUuid()).thenReturn("host-uuid-1");
@@ -2126,5 +2951,4 @@ void testGrantAccess_TemporarySnapshotCopy_SyncsIscsiPathOnInMemoryVolume() {
assertEquals(iscsiPath, inMemoryVolume.get_iScsiName());
}
}
-
}
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java
index 0a82cee17e6c..12ff93d71c11 100644
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/lifecycle/OntapPrimaryDatastoreLifecycleTest.java
@@ -23,6 +23,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
@@ -37,15 +38,18 @@
import com.cloud.dc.ClusterVO;
import com.cloud.host.HostVO;
import com.cloud.resource.ResourceManager;
+import com.cloud.storage.Storage;
import com.cloud.storage.StorageManager;
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.PrimaryDataStoreInfo;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreParameters;
import org.apache.cloudstack.engine.subsystem.api.storage.ZoneScope;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import com.cloud.hypervisor.Hypervisor;
import com.cloud.alert.AlertManager;
+import com.cloud.capacity.CapacityManager;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
@@ -57,11 +61,17 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.withSettings;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.ArgumentMatchers.contains;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.util.HashMap;
+import com.cloud.storage.StoragePool;
+import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreLifeCycle;
import org.apache.cloudstack.storage.provider.StorageProviderFactory;
import org.apache.cloudstack.storage.service.StorageStrategy;
import org.apache.cloudstack.storage.volume.datastore.PrimaryDataStoreHelper;
@@ -99,6 +109,9 @@ public class OntapPrimaryDatastoreLifecycleTest {
@Mock
private AlertManager _alertMgr;
+ @Mock
+ private CapacityManager _capacityMgr;
+
// Mock object that implements both DataStore and PrimaryDataStoreInfo
// This is needed because attachCluster(DataStore) casts DataStore to PrimaryDataStoreInfo internally
private DataStore dataStore;
@@ -123,6 +136,7 @@ void setUp() {
when(_clusterDao.findById(1L)).thenReturn(clusterVO);
when(storageStrategy.connect()).thenReturn(true);
+ when(storageStrategy.isAff()).thenReturn(true);
when(storageStrategy.getNetworkInterface()).thenReturn(new Pair<>("testNetworkInterface", null));
Volume volume = new Volume();
@@ -191,6 +205,112 @@ public void testInitialize_positive() {
}
}
+ private Map buildDsInfosForProtocol(String protocol) {
+ HashMap detailsMap = new HashMap();
+ detailsMap.put(OntapStorageConstants.USERNAME, "testUser");
+ detailsMap.put(OntapStorageConstants.PASSWORD, "testPassword");
+ detailsMap.put(OntapStorageConstants.STORAGE_IP, "10.10.10.10");
+ detailsMap.put(OntapStorageConstants.SVM_NAME, "vs0");
+ detailsMap.put(OntapStorageConstants.PROTOCOL, protocol);
+
+ Map dsInfos = new HashMap<>();
+ dsInfos.put("zoneId", 1L);
+ dsInfos.put("podId", 1L);
+ dsInfos.put("clusterId", 1L);
+ dsInfos.put("name", "testStoragePool");
+ dsInfos.put("providerName", "testProvider");
+ dsInfos.put("capacityBytes", 1073741824L);
+ dsInfos.put("managed", true);
+ dsInfos.put("tags", "testTag");
+ dsInfos.put("isTagARule", false);
+ dsInfos.put("details", detailsMap);
+ return dsInfos;
+ }
+
+ private Storage.StoragePoolType initializeAndCapturePoolType(String protocol) {
+ try (MockedStatic storageProviderFactory = Mockito.mockStatic(StorageProviderFactory.class)) {
+ storageProviderFactory.when(() -> StorageProviderFactory.getStrategy(any())).thenReturn(storageStrategy);
+ ontapPrimaryDatastoreLifecycle.initialize(buildDsInfosForProtocol(protocol));
+ }
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PrimaryDataStoreParameters.class);
+ verify(_dataStoreHelper).createPrimaryDataStore(captor.capture());
+ return captor.getValue().getType();
+ }
+
+ @Test
+ public void testInitialize_iscsiPoolUsesOntapIscsiType() {
+ when(storageStrategy.getStoragePath()).thenReturn("iqn.1992-08.com.netapp:sn.abc123");
+
+ assertEquals(Storage.StoragePoolType.OntapiSCSI, initializeAndCapturePoolType("ISCSI"));
+ }
+
+ @Test
+ public void testInitialize_nfsPoolKeepsNetworkFilesystemType() {
+ assertEquals(Storage.StoragePoolType.NetworkFilesystem, initializeAndCapturePoolType("NFS3"));
+ }
+
+ private Long initializeAndCaptureCapacityIops(Map dsInfos) {
+ try (MockedStatic storageProviderFactory = Mockito.mockStatic(StorageProviderFactory.class)) {
+ storageProviderFactory.when(() -> StorageProviderFactory.getStrategy(any())).thenReturn(storageStrategy);
+ ontapPrimaryDatastoreLifecycle.initialize(dsInfos);
+ }
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PrimaryDataStoreParameters.class);
+ verify(_dataStoreHelper).createPrimaryDataStore(captor.capture());
+ return captor.getValue().getCapacityIops();
+ }
+
+ @Test
+ public void testInitialize_blankCapacityIopsLeavesPoolWithoutIopsCeiling() {
+ assertNull(initializeAndCaptureCapacityIops(buildDsInfosForProtocol("NFS3")));
+ }
+
+ @Test
+ public void testInitialize_capacityIopsIsStoredOnPool() {
+ Map dsInfos = buildDsInfosForProtocol("NFS3");
+ dsInfos.put("capacityIops", 5000L);
+
+ assertEquals(Long.valueOf(5000L), initializeAndCaptureCapacityIops(dsInfos));
+ }
+
+ @Test
+ public void testInitialize_persistsAffPlatformDetail() {
+ Map details = initializeAndCaptureDetails(buildDsInfosForProtocol("NFS3"));
+
+ assertEquals("true", details.get(OntapStorageConstants.IS_AFF));
+ verify(storageStrategy).isAff();
+ }
+
+ @Test
+ public void testInitialize_persistsFasPlatformDetail() {
+ when(storageStrategy.isAff()).thenReturn(false);
+
+ Map details = initializeAndCaptureDetails(buildDsInfosForProtocol("NFS3"));
+
+ assertEquals("false", details.get(OntapStorageConstants.IS_AFF));
+ }
+
+ private Map initializeAndCaptureDetails(Map dsInfos) {
+ try (MockedStatic storageProviderFactory = Mockito.mockStatic(StorageProviderFactory.class)) {
+ storageProviderFactory.when(() -> StorageProviderFactory.getStrategy(any())).thenReturn(storageStrategy);
+ ontapPrimaryDatastoreLifecycle.initialize(dsInfos);
+ }
+ ArgumentCaptor captor = ArgumentCaptor.forClass(PrimaryDataStoreParameters.class);
+ verify(_dataStoreHelper).createPrimaryDataStore(captor.capture());
+ return captor.getValue().getDetails();
+ }
+
+ @Test
+ public void testInitialize_nonPositiveCapacityIopsIsRejected() {
+ Map dsInfos = buildDsInfosForProtocol("NFS3");
+ dsInfos.put("capacityIops", 0L);
+
+ Exception ex = assertThrows(InvalidParameterValueException.class,
+ () -> ontapPrimaryDatastoreLifecycle.initialize(dsInfos));
+
+ assertTrue(ex.getMessage().contains("IOPS capacity must be greater than 0"));
+ verify(_dataStoreHelper, never()).createPrimaryDataStore(any());
+ }
+
@Test
public void testInitialize_null_Arg() {
Exception ex = assertThrows(CloudRuntimeException.class,() ->
@@ -1121,4 +1241,142 @@ public void testAttachZone_kvmHypervisorSetsAndUpdatesPool() throws Exception {
}
}
+ // ========== updateStoragePool() Tests ==========
+
+ @Test
+ public void testUpdateStoragePool_positive_resizesFlexVolume() {
+ // Setup
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getName()).thenReturn("test-pool");
+ when(storagePool.getCapacityBytes()).thenReturn(2147483648L); // 2 GB current
+
+ Map details = new HashMap<>();
+ details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(5368709120L)); // 5 GB new
+ details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123");
+ details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-name");
+ details.put("protocol", "NFS3");
+
+ Volume updatedVolume = new Volume();
+ updatedVolume.setUuid("flex-vol-uuid-123");
+ updatedVolume.setSize(5368709120L);
+ when(storageStrategy.updateStorageVolume(any(Volume.class))).thenReturn(updatedVolume);
+
+ try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(storageStrategy);
+
+ // Execute
+ ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details);
+
+ // Verify
+ verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class));
+ }
+ }
+
+ @Test
+ public void testUpdateStoragePool_noCapacityBytesInDetails_skipsResize() {
+ // Setup
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getName()).thenReturn("test-pool");
+
+ Map details = new HashMap<>();
+ details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123");
+ details.put("protocol", "NFS3");
+ // No CAPACITY_BYTES key — resize should be skipped
+
+ // Execute
+ ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details);
+
+ // Verify — storageStrategy should never be called
+ verify(storageStrategy, never()).updateStorageVolume(any());
+ }
+
+ @Test
+ public void testUpdateStoragePool_missingVolumeUuid_throwsCloudRuntimeException() {
+ // Setup
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getName()).thenReturn("test-pool");
+ when(storagePool.getCapacityBytes()).thenReturn(1073741824L);
+
+ Map details = new HashMap<>();
+ details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L));
+ details.put("protocol", "NFS3");
+ // No VOLUME_UUID — cannot resize without it
+
+ try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(storageStrategy);
+
+ // Execute & Verify
+ assertThrows(CloudRuntimeException.class,
+ () -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details));
+ verify(storageStrategy, never()).updateStorageVolume(any());
+ }
+ }
+
+ @Test
+ public void testUpdateStoragePool_capacityIopsBelowAllocated_throwsInvalidParameterValueException() {
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("test-pool");
+
+ Map details = new HashMap<>();
+ details.put(PrimaryDataStoreLifeCycle.CAPACITY_IOPS, "400");
+ details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-123");
+ details.put("protocol", "NFS3");
+
+ when(_capacityMgr.getUsedIops(any(StoragePoolVO.class))).thenReturn(900L);
+
+ Exception ex = assertThrows(InvalidParameterValueException.class,
+ () -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details));
+
+ assertTrue(ex.getMessage().contains("900 IOPS are already allocated"));
+ verify(storageStrategy, never()).updateStorageVolume(any(Volume.class));
+ }
+
+ @Test
+ public void testUpdateStoragePool_capacityIopsAboveAllocated_isAccepted() {
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getId()).thenReturn(1L);
+ when(storagePool.getName()).thenReturn("test-pool");
+
+ Map details = new HashMap<>();
+ details.put(PrimaryDataStoreLifeCycle.CAPACITY_IOPS, "2000");
+ details.put("protocol", "NFS3");
+ // No CAPACITY_BYTES key — only the IOPS ceiling is being raised.
+
+ when(_capacityMgr.getUsedIops(any(StoragePoolVO.class))).thenReturn(900L);
+
+ ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details);
+
+ verify(storageStrategy, never()).updateStorageVolume(any(Volume.class));
+ }
+
+ @Test
+ public void testUpdateStoragePool_updateStorageVolumeThrows_propagatesCloudRuntimeException() {
+ // Setup
+ StoragePool storagePool = mock(StoragePool.class);
+ when(storagePool.getName()).thenReturn("test-pool");
+ when(storagePool.getCapacityBytes()).thenReturn(1073741824L);
+
+ Map details = new HashMap<>();
+ details.put(PrimaryDataStoreLifeCycle.CAPACITY_BYTES, String.valueOf(3221225472L));
+ details.put(OntapStorageConstants.VOLUME_UUID, "flex-vol-uuid-err");
+ details.put(OntapStorageConstants.VOLUME_NAME, "flexvol-err");
+ details.put("protocol", "NFS3");
+
+ when(storageStrategy.updateStorageVolume(any(Volume.class)))
+ .thenThrow(new CloudRuntimeException("ONTAP resize failed"));
+
+ try (MockedStatic utilityMock = Mockito.mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
+ .thenReturn(storageStrategy);
+
+ // Execute & Verify
+ assertThrows(CloudRuntimeException.class,
+ () -> ontapPrimaryDatastoreLifecycle.updateStoragePool(storagePool, details));
+ verify(storageStrategy, times(1)).updateStorageVolume(any(Volume.class));
+ }
+ }
+
}
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 59df4eadfe9a..db5f0a33b307 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
@@ -27,6 +27,7 @@
import java.util.Map;
import org.apache.cloudstack.storage.feign.client.AggregateFeignClient;
+import org.apache.cloudstack.storage.feign.client.ClusterFeignClient;
import org.apache.cloudstack.storage.feign.client.JobFeignClient;
import org.apache.cloudstack.storage.feign.client.NetworkFeignClient;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
@@ -34,6 +35,8 @@
import org.apache.cloudstack.storage.feign.client.SvmFeignClient;
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.Aggregate;
+import org.apache.cloudstack.storage.feign.model.Cluster;
+import org.apache.cloudstack.storage.feign.model.ClusterNode;
import org.apache.cloudstack.storage.feign.model.IpInterface;
import org.apache.cloudstack.storage.feign.model.IscsiService;
import org.apache.cloudstack.storage.feign.model.Job;
@@ -47,7 +50,9 @@
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
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.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
@@ -59,10 +64,8 @@
import static org.mockito.ArgumentMatchers.eq;
import org.mockito.Mock;
import static org.mockito.Mockito.atLeastOnce;
-import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -101,6 +104,9 @@ public class StorageStrategyTest {
@Mock
private SnapshotFeignClient snapshotFeignClient;
+ @Mock
+ private ClusterFeignClient clusterFeignClient;
+
private TestableStorageStrategy storageStrategy;
// Concrete implementation for testing abstract class
@@ -112,7 +118,8 @@ public TestableStorageStrategy(OntapStorage ontapStorage,
JobFeignClient jobFeignClient,
NetworkFeignClient networkFeignClient,
SANFeignClient sanFeignClient,
- SnapshotFeignClient snapshotFeignClient) {
+ SnapshotFeignClient snapshotFeignClient,
+ ClusterFeignClient clusterFeignClient) {
super(ontapStorage);
// Use reflection to replace the private Feign client fields with mocked ones
injectMockedClient("aggregateFeignClient", aggregateFeignClient);
@@ -122,6 +129,7 @@ public TestableStorageStrategy(OntapStorage ontapStorage,
injectMockedClient("networkFeignClient", networkFeignClient);
injectMockedClient("sanFeignClient", sanFeignClient);
injectMockedClient("snapshotFeignClient", snapshotFeignClient);
+ injectMockedClient("clusterFeignClient", clusterFeignClient);
}
private void injectMockedClient(String fieldName, Object mockedClient) {
@@ -147,7 +155,7 @@ public CloudStackVolume createTemplateCache(org.apache.cloudstack.storage.datast
}
@Override
- CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
+ public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
return null;
}
@@ -218,7 +226,8 @@ void setUp() {
// For testing, we'll need to mock the FeignClientFactory behavior
storageStrategy = new TestableStorageStrategy(ontapStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
}
// ========== connect() Tests ==========
@@ -277,6 +286,154 @@ public void testConnect_operationsOnly_skipsAggregateValidation() {
assertTrue(result, "connect() should succeed for an online aggregate even when its free space is below the pool capacity");
}
+ @Test
+ public void testGetClusterInfo_dedupesIdenticalNodeModels() {
+ Cluster cluster = stubClusterGet();
+ ClusterNode node1 = new ClusterNode();
+ node1.setModel("AFF-A400");
+ ClusterNode node2 = new ClusterNode();
+ node2.setModel("AFF-A400");
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(node1, node2)));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertEquals(cluster, result);
+ assertEquals("AFF-A400", result.getModel());
+ }
+
+ @Test
+ public void testGetClusterInfo_joinsDistinctModels() {
+ stubClusterGet();
+ ClusterNode node1 = new ClusterNode();
+ node1.setModel("AFF-A400");
+ ClusterNode node2 = new ClusterNode();
+ node2.setModel("FAS8300");
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(node1, node2)));
+
+ assertEquals("AFF-A400,FAS8300", storageStrategy.getClusterInfo().getModel());
+ }
+
+ @Test
+ public void testGetClusterInfo_allFlashPerformance() {
+ stubClusterGet();
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("AFF-A400", true, true, false))));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertEquals("AFF-A400", result.getModel());
+ assertEquals(OntapStorageConstants.ASUP_PLATFORM_TYPE_PERFORMANCE, result.getPlatformType());
+ }
+
+ @Test
+ public void testGetClusterInfo_allFlashCapacity() {
+ stubClusterGet();
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-C800", true, false, true),
+ clusterNode("AFF-C800", true, false, true))));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertEquals("AFF-C800", result.getModel());
+ assertEquals(OntapStorageConstants.ASUP_PLATFORM_TYPE_CAPACITY, result.getPlatformType());
+ }
+
+ @Test
+ public void testGetClusterInfo_notAllFlashIsFas() {
+ stubClusterGet();
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("FAS8300", false, false, false),
+ clusterNode("FAS8300", false, false, false))));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertEquals("FAS8300", result.getModel());
+ assertEquals(OntapStorageConstants.ASUP_PLATFORM_TYPE_FAS, result.getPlatformType());
+ }
+
+ @Test
+ public void testGetClusterInfo_mixedPersonalitiesIsComposite() {
+ stubClusterGet();
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("FAS8300", false, false, false),
+ clusterNode("FAS8300", false, false, false))));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertEquals("AFF-A400,FAS8300", result.getModel());
+ assertEquals(OntapStorageConstants.ASUP_PLATFORM_TYPE_COMPOSITE, result.getPlatformType());
+ }
+
+ @Test
+ public void testGetClusterInfo_nodesGetFailureLeavesModelUnset() {
+ stubClusterGet();
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenThrow(new RuntimeException("connection refused"));
+
+ Cluster result = storageStrategy.getClusterInfo();
+ assertNotNull(result);
+ assertNull(result.getModel());
+ assertNull(result.getPlatformType());
+ }
+
+ @Test
+ public void testIsAff_allNodesAllFlash_returnsTrue() {
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("AFF-A400", true, true, false))));
+
+ assertTrue(storageStrategy.isAff());
+ }
+
+ @Test
+ public void testIsAff_anyNodeNotAllFlash_returnsFalse() {
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("FAS8300", false, false, false))));
+
+ assertFalse(storageStrategy.isAff());
+ }
+
+ @Test
+ public void testIsAff_nullAllFlashOptimized_returnsFalse() {
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of(
+ clusterNode("AFF-A400", true, true, false),
+ clusterNode("unknown", null, null, null))));
+
+ assertFalse(storageStrategy.isAff());
+ }
+
+ @Test
+ public void testIsAff_noNodes_throws() {
+ when(clusterFeignClient.getClusterNodes(anyString(), anyMap()))
+ .thenReturn(new OntapResponse<>(List.of()));
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> storageStrategy.isAff());
+ assertTrue(ex.getMessage().contains("Unable to determine whether the ONTAP cluster is AFF or FAS"));
+ }
+
+ private Cluster stubClusterGet() {
+ Cluster cluster = new Cluster();
+ when(clusterFeignClient.getCluster(anyString(), eq(true))).thenReturn(cluster);
+ return cluster;
+ }
+
+ private static ClusterNode clusterNode(String model, Boolean allFlash, Boolean performance, Boolean capacity) {
+ ClusterNode node = new ClusterNode();
+ node.setModel(model);
+ node.setAllFlashOptimized(allFlash);
+ node.setPerformanceOptimized(performance);
+ node.setCapacityOptimized(capacity);
+ return node;
+ }
+
@Test
public void testConnect_noOnlineAggregates() {
// Setup - aggregate assigned to the SVM exists but is not ONLINE
@@ -372,7 +529,8 @@ public void testConnect_iscsiNotEnabled() {
"svm1", 5000000000L, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
Svm svm = new Svm();
svm.setName("svm1");
@@ -670,39 +828,6 @@ public void testDeleteStorageVolume_jobFailed() {
assertTrue(ex.getMessage().contains("Job failed"));
}
- @Test
- public void testPollJobIfPresent_failedPollThrowsWithOperationAndJobUuid() {
- TestableStorageStrategy spyStrategy = spy(storageStrategy);
- doReturn(false).when(spyStrategy).jobPollForSuccess(eq("job-uuid-1"), eq(60), eq(2000));
-
- Job job = new Job();
- job.setUuid("job-uuid-1");
- JobResponse jobResponse = new JobResponse();
- jobResponse.setJob(job);
-
- CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
- () -> spyStrategy.pollJobIfPresent(jobResponse, "clone file"));
- assertTrue(ex.getMessage().contains("job-uuid-1"));
- assertTrue(ex.getMessage().contains("clone file"));
- }
-
- @Test
- public void testPollJobIfPresent_noJobSkipsPolling() {
- TestableStorageStrategy spyStrategy = spy(storageStrategy);
-
- spyStrategy.pollJobIfPresent(new JobResponse(), "clone file");
- spyStrategy.pollJobIfPresent(null, "clone file");
-
- verify(spyStrategy, never()).jobPollForSuccess(anyString(), eq(60), eq(2000));
- }
-
- @Test
- public void testDeleteFileByPath_unsupportedStrategyThrows() {
- CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
- () -> storageStrategy.deleteFileByPath("flexvol-uuid", "template-uuid"));
- assertTrue(ex.getMessage().contains("TestableStorageStrategy"));
- }
-
@Test
public void testDeleteStorageVolume_feignException() {
// Setup
@@ -749,7 +874,8 @@ public void testGetStoragePath_iscsi() {
"svm1", null, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
IscsiService.IscsiServiceTarget target = new IscsiService.IscsiServiceTarget();
target.setName("iqn.1992-08.com.netapp:sn.123456:vs.1");
@@ -779,7 +905,8 @@ public void testGetStoragePath_iscsi_noService() {
"svm1", null, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
OntapResponse emptyResponse = new OntapResponse<>();
emptyResponse.setRecords(new ArrayList<>());
@@ -800,7 +927,8 @@ public void testGetStoragePath_iscsi_noTargetIqn() {
"svm1", null, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
IscsiService iscsiService = new IscsiService();
iscsiService.setTarget(null);
@@ -853,7 +981,8 @@ public void testGetNetworkInterface_iscsi() {
"svm1", null, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
IpInterface.IpInfo ipInfo = new IpInterface.IpInfo();
ipInfo.setAddress("192.168.1.51");
@@ -929,7 +1058,8 @@ public void testGetNetworkInterface_iscsi_lifDown() {
"svm1", null, ProtocolType.ISCSI);
storageStrategy = new TestableStorageStrategy(iscsiStorage,
aggregateFeignClient, volumeFeignClient, svmFeignClient,
- jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient);
+ jobFeignClient, networkFeignClient, sanFeignClient, snapshotFeignClient,
+ clusterFeignClient);
IpInterface.IpInfo ipInfo = new IpInterface.IpInfo();
ipInfo.setAddress("192.168.1.51");
@@ -1293,16 +1423,117 @@ void testDeleteFlexVolSnapshotForCloudStackVolume_AlreadyAbsentOnOntap() {
verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1"));
}
+ @Test
+ void testDeleteFlexVolSnapshotForCloudStackVolume_Feign404_TreatedAsSuccess() {
+ FeignException notFoundException = mock(FeignException.class);
+ when(notFoundException.status()).thenReturn(404);
+ when(snapshotFeignClient.deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1")))
+ .thenThrow(notFoundException);
+
+ storageStrategy.deleteFlexVolSnapshotForCloudStackVolume("fv-uuid-1", "snap-uuid-1", "snap-name-1");
+
+ verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1"));
+ verify(jobFeignClient, never()).getJobByUUID(anyString(), anyString());
+ }
+
+ // ========== updateStorageVolume() Tests ==========
+
@Test
- void testDeleteFlexVolSnapshotForCloudStackVolume_Feign404_TreatedAsSuccess() {
- FeignException notFoundException = mock(FeignException.class);
- when(notFoundException.status()).thenReturn(404);
- when(snapshotFeignClient.deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1")))
- .thenThrow(notFoundException);
+ public void testUpdateStorageVolume_positive() {
+ // Setup
+ Volume volume = new Volume();
+ volume.setUuid("vol-uuid-resize");
+ volume.setName("flexvol-resize");
+ volume.setSize(5368709120L); // 5 GB
- storageStrategy.deleteFlexVolSnapshotForCloudStackVolume("fv-uuid-1", "snap-uuid-1", "snap-name-1");
+ Job job = new Job();
+ job.setUuid("resize-job-uuid");
+ JobResponse jobResponse = new JobResponse();
+ jobResponse.setJob(job);
- verify(snapshotFeignClient).deleteSnapshot(anyString(), eq("fv-uuid-1"), eq("snap-uuid-1"));
- verify(jobFeignClient, never()).getJobByUUID(anyString(), anyString());
+ when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-resize"), any()))
+ .thenReturn(jobResponse);
+
+ Job completedJob = new Job();
+ completedJob.setUuid("resize-job-uuid");
+ completedJob.setState(OntapStorageConstants.JOB_SUCCESS);
+ when(jobFeignClient.getJobByUUID(anyString(), eq("resize-job-uuid")))
+ .thenReturn(completedJob);
+
+ // Execute
+ Volume result = storageStrategy.updateStorageVolume(volume);
+
+ // Verify
+ assertNotNull(result);
+ assertEquals(5368709120L, result.getSize());
+ verify(volumeFeignClient, times(1)).updateVolume(anyString(), eq("vol-uuid-resize"), any());
+ verify(jobFeignClient, atLeastOnce()).getJobByUUID(anyString(), eq("resize-job-uuid"));
+ }
+
+ @Test
+ public void testUpdateStorageVolume_jobFailed() {
+ // Setup
+ Volume volume = new Volume();
+ volume.setUuid("vol-uuid-resize");
+ volume.setName("flexvol-resize");
+ volume.setSize(5368709120L);
+
+ Job job = new Job();
+ job.setUuid("resize-job-uuid");
+ JobResponse jobResponse = new JobResponse();
+ jobResponse.setJob(job);
+
+ when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-resize"), any()))
+ .thenReturn(jobResponse);
+
+ Job failedJob = new Job();
+ failedJob.setUuid("resize-job-uuid");
+ failedJob.setState(OntapStorageConstants.JOB_FAILURE);
+ failedJob.setMessage("Resize failed");
+ when(jobFeignClient.getJobByUUID(anyString(), eq("resize-job-uuid")))
+ .thenReturn(failedJob);
+
+ // Execute & Verify
+ Exception ex = assertThrows(CloudRuntimeException.class,
+ () -> storageStrategy.updateStorageVolume(volume));
+ assertTrue(ex.getMessage().contains("Job failed"));
+ }
+
+ @Test
+ public void testUpdateStorageVolume_feignException() {
+ // Setup
+ Volume volume = new Volume();
+ volume.setUuid("vol-uuid-fail");
+ volume.setName("flexvol-fail");
+ volume.setSize(3221225472L);
+
+ FeignException feignException = mock(FeignException.class);
+ when(feignException.status()).thenReturn(500);
+ when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-fail"), any()))
+ .thenThrow(feignException);
+
+ // Execute & Verify
+ Exception ex = assertThrows(CloudRuntimeException.class,
+ () -> storageStrategy.updateStorageVolume(volume));
+ assertTrue(ex.getMessage().contains("Failed to resize ONTAP FlexVolume"));
+ }
+
+ @Test
+ public void testUpdateStorageVolume_notFound_404_throwsCloudRuntimeException() {
+ // Setup
+ Volume volume = new Volume();
+ volume.setUuid("vol-uuid-notfound");
+ volume.setName("flexvol-notfound");
+ volume.setSize(1073741824L);
+
+ FeignException feignEx = mock(FeignException.class);
+ when(feignEx.status()).thenReturn(404);
+ when(volumeFeignClient.updateVolume(anyString(), eq("vol-uuid-notfound"), any()))
+ .thenThrow(feignEx);
+
+ // Execute & Verify — 404 means volume not found on ONTAP, should throw
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
+ () -> storageStrategy.updateStorageVolume(volume));
+ assertTrue(ex.getMessage().contains("not found on ONTAP"));
}
}
diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
index fa171a52fa29..e4888be222b1 100755
--- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
+++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/service/UnifiedNASStrategyTest.java
@@ -30,6 +30,7 @@
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.command.CreateObjectCommand;
+import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
@@ -46,6 +47,7 @@
import org.apache.cloudstack.storage.feign.model.ExportRule;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -78,6 +80,7 @@
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -228,6 +231,106 @@ public void testCreateTemplateCache_IsNoOp() {
assertNull(result);
}
+
+ @Test
+ public void testCreateCloudStackVolume_AppliesQosPolicyToNfsFile() throws Exception {
+ CloudStackVolume cloudStackVolume = mock(CloudStackVolume.class);
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ EndPoint endPoint = mock(EndPoint.class);
+ Answer answer = new Answer(null, true, "Success");
+
+ VolumeQosPolicy qosPolicy = new VolumeQosPolicy();
+ qosPolicy.setName("cs_100_to200_iops_svm1");
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(qosPolicy);
+
+ when(cloudStackVolume.getDatastoreId()).thenReturn("1");
+ when(cloudStackVolume.getVolumeInfo()).thenReturn(volumeObject);
+ when(cloudStackVolume.getFlexVolumeUuid()).thenReturn("flex-uuid");
+ when(cloudStackVolume.getFile()).thenReturn(fileInfo);
+ when(volumeObject.getId()).thenReturn(100L);
+ when(volumeObject.getUuid()).thenReturn("volume-uuid-123");
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDao.update(anyLong(), any(VolumeVO.class))).thenReturn(true);
+ when(epSelector.select(volumeObject)).thenReturn(endPoint);
+ when(endPoint.sendMessage(any(CreateObjectCommand.class))).thenReturn(answer);
+
+ CloudStackVolume result = strategy.createCloudStackVolume(cloudStackVolume);
+
+ assertNotNull(result);
+ verify(nasFeignClient).updateFile(anyString(), eq("flex-uuid"), eq("volume-uuid-123"),
+ argThat(file -> file.getQosPolicy() != null
+ && "cs_100_to200_iops_svm1".equals(file.getQosPolicy().getName())));
+ }
+
+ @Test
+ public void testCreateCloudStackVolume_QosAttachFails_DeletesLeftoverNfsFile() {
+ CloudStackVolume cloudStackVolume = mock(CloudStackVolume.class);
+ VolumeObject volumeObject = mock(VolumeObject.class);
+ VolumeVO volumeVO = mock(VolumeVO.class);
+ EndPoint endPoint = mock(EndPoint.class);
+ Answer createAnswer = new Answer(null, true, "Success");
+ Answer deleteAnswer = new Answer(null, true, "Deleted");
+
+ VolumeQosPolicy qosPolicy = new VolumeQosPolicy();
+ qosPolicy.setName("cs_100_to200_iops_svm1");
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(qosPolicy);
+
+ when(cloudStackVolume.getDatastoreId()).thenReturn("1");
+ when(cloudStackVolume.getVolumeInfo()).thenReturn(volumeObject);
+ when(cloudStackVolume.getFlexVolumeUuid()).thenReturn("flex-uuid");
+ when(cloudStackVolume.getFile()).thenReturn(fileInfo);
+ when(volumeObject.getId()).thenReturn(100L);
+ when(volumeObject.getUuid()).thenReturn("volume-uuid-123");
+ when(volumeDao.findById(100L)).thenReturn(volumeVO);
+ when(volumeDao.update(anyLong(), any(VolumeVO.class))).thenReturn(true);
+ when(epSelector.select(volumeObject)).thenReturn(endPoint);
+ when(endPoint.sendMessage(any(CreateObjectCommand.class))).thenReturn(createAnswer);
+ when(endPoint.sendMessage(any(DeleteCommand.class))).thenReturn(deleteAnswer);
+
+ FeignException feignException = mock(FeignException.class);
+ when(feignException.contentUTF8()).thenReturn(
+ "{\"error\":{\"code\":\"8454269\",\"message\":\"Invalid QoS policy group specified\"}}");
+ when(feignException.getMessage()).thenReturn("Bad Request");
+ doThrow(feignException).when(nasFeignClient).updateFile(anyString(), eq("flex-uuid"),
+ eq("volume-uuid-123"), any(FileInfo.class));
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
+ () -> strategy.createCloudStackVolume(cloudStackVolume));
+ assertTrue(ex.getMessage().contains("Failed to apply QoS policy to NFS volume file"));
+ verify(endPoint).sendMessage(any(DeleteCommand.class));
+ }
+
+ @Test
+ public void testUpdateCloudStackVolume_AppliesQosPolicy() {
+ VolumeInfo volumeInfo = mock(VolumeInfo.class);
+ when(volumeInfo.getUuid()).thenReturn("volume-uuid-123");
+
+ VolumeQosPolicy qosPolicy = new VolumeQosPolicy();
+ qosPolicy.setName("cs_100_to200_iops_svm1");
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(qosPolicy);
+
+ CloudStackVolume request = new CloudStackVolume();
+ request.setVolumeInfo(volumeInfo);
+ request.setFlexVolumeUuid("flex-uuid");
+ request.setFile(fileInfo);
+
+ CloudStackVolume result = strategy.updateCloudStackVolume(request);
+
+ assertSame(request, result);
+ verify(nasFeignClient).updateFile(anyString(), eq("flex-uuid"), eq("volume-uuid-123"),
+ argThat(file -> file.getQosPolicy() != null
+ && "cs_100_to200_iops_svm1".equals(file.getQosPolicy().getName())));
+ }
+
+ @Test
+ public void testUpdateCloudStackVolume_InvalidRequest_ThrowsException() {
+ assertThrows(CloudRuntimeException.class, () -> strategy.updateCloudStackVolume(new CloudStackVolume()));
+ }
+
// Test createCloudStackVolume - Volume Not Found
@Test
public void testCreateCloudStackVolume_VolumeNotFound() {
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 02b1e5263d50..3f92679274f6 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
@@ -30,6 +30,7 @@
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.feign.model.LunMap;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
@@ -54,11 +55,13 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
@@ -389,6 +392,28 @@ void testCreateCloudStackVolume_FeignException_ThrowsCloudRuntimeException() {
}
}
+ @Test
+ void testCreateCloudStackVolume_MinThroughputRejected_PropagatesOntapError() {
+ Lun lun = new Lun();
+ lun.setName("/vol/vol1/lun1");
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lun);
+
+ FeignException feignException = mock(FeignException.class);
+ when(feignException.getMessage()).thenReturn("Bad Request");
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
+ .thenReturn(authHeader);
+ when(sanFeignClient.createLun(eq(authHeader), eq(true), any(Lun.class)))
+ .thenThrow(feignException);
+
+ CloudRuntimeException ex = assertThrows(CloudRuntimeException.class,
+ () -> unifiedSANStrategy.createCloudStackVolume(request));
+ assertTrue(ex.getMessage().contains("Failed to create Lun"));
+ }
+ }
+
@Test
void testDeleteCloudStackVolume_Success() {
// Setup
@@ -402,7 +427,7 @@ void testDeleteCloudStackVolume_Success() {
utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
.thenReturn(authHeader);
- doNothing().when(sanFeignClient).deleteLun(eq(authHeader), eq("lun-uuid-123"), anyMap());
+ when(sanFeignClient.deleteLun(eq(authHeader), eq("lun-uuid-123"), anyMap())).thenReturn(null);
// Execute
unifiedSANStrategy.deleteCloudStackVolume(request);
@@ -1118,7 +1143,6 @@ void testResizeCloudStackVolume_FeignException_Throws() {
request.setLun(lun);
FeignException feignException = mock(FeignException.class);
- when(feignException.status()).thenReturn(500);
when(feignException.getMessage()).thenReturn("resize failed");
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
@@ -1191,10 +1215,35 @@ void testSetOntapStorage() {
}
@Test
- void testUpdateCloudStackVolume_ReturnsNull() {
+ void testUpdateCloudStackVolume_InvalidRequest_ThrowsException() {
CloudStackVolume request = new CloudStackVolume();
- CloudStackVolume result = unifiedSANStrategy.updateCloudStackVolume(request);
- assertNull(result);
+ assertThrows(CloudRuntimeException.class,
+ () -> unifiedSANStrategy.updateCloudStackVolume(request));
+ }
+
+ @Test
+ void testUpdateCloudStackVolume_AppliesQosPolicyToLun() {
+ Lun lun = new Lun();
+ lun.setUuid("lun-uuid-123");
+ VolumeQosPolicy qosPolicy = new VolumeQosPolicy();
+ qosPolicy.setName("cs_0_to5000_iops_svm1");
+ lun.setQosPolicy(qosPolicy);
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lun);
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
+ .thenReturn(authHeader);
+ when(sanFeignClient.updateLun(eq(authHeader), eq("lun-uuid-123"), any(Lun.class)))
+ .thenReturn(null);
+
+ CloudStackVolume result = unifiedSANStrategy.updateCloudStackVolume(request);
+
+ assertSame(request, result);
+ verify(sanFeignClient).updateLun(eq(authHeader), eq("lun-uuid-123"), argThat(update ->
+ update.getQosPolicy() != null
+ && "cs_0_to5000_iops_svm1".equals(update.getQosPolicy().getName())));
+ }
}
@Test
@@ -2251,4 +2300,35 @@ void testEnsureLunMapped_ExistingMapping_ReturnsExistingNumber() {
verify(sanFeignClient, never()).createLunMap(any(), anyBoolean(), any(LunMap.class));
}
}
+
+ @Test
+ void testCreateCloudStackVolume_PassesQosPolicyOnLunCreate() {
+ Lun lun = new Lun();
+ lun.setName("/vol/vol1/lun1");
+ VolumeQosPolicy qosPolicy = new VolumeQosPolicy();
+ qosPolicy.setName("cs_100_to200_iops_svm1");
+ lun.setQosPolicy(qosPolicy);
+ CloudStackVolume request = new CloudStackVolume();
+ request.setLun(lun);
+
+ Lun createdLun = new Lun();
+ createdLun.setName("/vol/vol1/lun1");
+ createdLun.setUuid("lun-uuid-123");
+ OntapResponse response = new OntapResponse<>();
+ response.setRecords(List.of(createdLun));
+
+ try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) {
+ utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password"))
+ .thenReturn(authHeader);
+ when(sanFeignClient.createLun(eq(authHeader), eq(true), any(Lun.class)))
+ .thenReturn(response);
+
+ unifiedSANStrategy.createCloudStackVolume(request);
+
+ ArgumentCaptor lunCaptor = ArgumentCaptor.forClass(Lun.class);
+ verify(sanFeignClient).createLun(eq(authHeader), eq(true), lunCaptor.capture());
+ assertNotNull(lunCaptor.getValue().getQosPolicy());
+ assertEquals("cs_100_to200_iops_svm1", lunCaptor.getValue().getQosPolicy().getName());
+ }
+ }
}
diff --git a/ui/src/views/infra/AddPrimaryStorage.vue b/ui/src/views/infra/AddPrimaryStorage.vue
index eeb3ebdd22c6..ec155d6dcd72 100644
--- a/ui/src/views/infra/AddPrimaryStorage.vue
+++ b/ui/src/views/infra/AddPrimaryStorage.vue
@@ -301,6 +301,12 @@
+
+
+
+
+
+
diff --git a/ui/src/views/infra/zone/ZoneWizardAddResources.vue b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
index 9bd9c6d37aef..faf9a10679f6 100644
--- a/ui/src/views/infra/zone/ZoneWizardAddResources.vue
+++ b/ui/src/views/infra/zone/ZoneWizardAddResources.vue
@@ -611,7 +611,7 @@ export default {
title: 'label.capacityiops',
key: 'capacityIops',
hidden: {
- provider: ['DefaultPrimary', 'PowerFlex', 'Linstor', 'NetApp ONTAP']
+ provider: ['DefaultPrimary', 'PowerFlex', 'Linstor']
}
},
{
diff --git a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
index f21201572dff..9402fe99a9c9 100644
--- a/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
+++ b/ui/src/views/infra/zone/ZoneWizardLaunchZone.vue
@@ -1619,6 +1619,9 @@ export default {
if (this.prefillContent.capacityBytes && this.prefillContent.capacityBytes.length > 0) {
params.capacityBytes = this.prefillContent.capacityBytes.split(',').join('')
}
+ if (this.prefillContent.capacityIops && this.prefillContent.capacityIops.length > 0) {
+ params.capacityIops = this.prefillContent.capacityIops.split(',').join('')
+ }
}
params.tags = this.prefillContent?.primaryStorageTags || ''
From 1fccc42f4f0f69b79cfaa363e0c5ff6bd561ba63 Mon Sep 17 00:00:00 2001
From: Sathvika
Date: Mon, 12 Oct 2026 10:43:02 +0530
Subject: [PATCH 6/9] CSTACKEX-261: enabling cloudstack volume resize (GROW)
for iscsi and nfs (#100)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Cloudstack volume resize supports grow for both NFS and Iscsi which on
ontap leads to increase in size for a LUN in case of iscsi and file in
case of NFS.Also consists of UT's for the changes and implementation for
getusedbytes which is called in this flow to check if storagepool has
enough space.
This PR...
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Enhancement (improves an existing feature and functionality)
- [ ] Cleanup (Code refactoring and cleanup, that may add test cases)
- [ ] Build/CI
- [ ] Test (unit or integration test code)
- [ ] Major
- [X] Minor
- [ ] BLOCKER
- [ ] Critical
- [ ] Major
- [ ] Minor
- [ ] Trivial
Before resizing volumes:
Growing iscsi csvolume to 10GiB:
Lun on Ontap is grown to 10GiB:
Growing Nfs volume to 10GiB:
virtual size of file on host side grown to 10GiB:
maxsize by default is 1024 GiB in cloudstack for volume which is
configurable if size greater than that throws an error:
considering the overprovisiong factor, used space and current space,
cloud stack calculates and propagates error in case value is over that:
here get used bytes gets triggered:
ISCSI CS-volume resize and get usedbytes here is triggered.
NFS CS-volume resize and get usedbytes here is triggered.
when Lun is deleted on ontap and cs volume is tried to resize , error is
thrown:
when file is deleted on ONtap and resize is performed on volume:
when shrink is performed on Iscsi volume with shrinkOk as true:
when shrink is performed on NFS volume :
change?
---------
Co-authored-by: Gupta, Surya
---
.../command/user/volume/ResizeVolumeCmd.java | 6 +-
.../driver/OntapPrimaryDatastoreDriver.java | 257 +++--
.../feign/client/VolumeFeignClient.java | 2 +-
.../storage/service/StorageStrategy.java | 51 +-
.../storage/service/UnifiedNASStrategy.java | 15 +-
.../storage/service/UnifiedSANStrategy.java | 92 +-
.../storage/utils/OntapStorageConstants.java | 8 +-
.../OntapPrimaryDatastoreDriverTest.java | 681 +++++++++++--
.../storage/service/StorageStrategyTest.java | 56 +-
.../service/UnifiedNASStrategyTest.java | 951 +++++++++++-------
.../service/UnifiedSANStrategyTest.java | 228 ++++-
11 files changed, 1701 insertions(+), 646 deletions(-)
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
index f8f744285c04..60f4f308503d 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/user/volume/ResizeVolumeCmd.java
@@ -15,14 +15,14 @@
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.user.volume;
-import org.apache.cloudstack.api.BaseAsyncCmd;
-
import org.apache.cloudstack.acl.SecurityChecker.AccessType;
import org.apache.cloudstack.api.ACL;
import org.apache.cloudstack.api.APICommand;
+import org.apache.cloudstack.api.ApiArgValidator;
import org.apache.cloudstack.api.ApiCommandResourceType;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
+import org.apache.cloudstack.api.BaseAsyncCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject.ResponseView;
import org.apache.cloudstack.api.ServerApiException;
@@ -60,7 +60,7 @@ public class ResizeVolumeCmd extends BaseAsyncCmd implements UserCmd {
@Parameter(name = ApiConstants.MAX_IOPS, type = CommandType.LONG, required = false, description = "New maximum number of IOPS")
private Long maxIops;
- @Parameter(name = ApiConstants.SIZE, type = CommandType.LONG, required = false, description = "New volume size in GB")
+ @Parameter(name = ApiConstants.SIZE, type = CommandType.LONG, required = false, description = "New volume size in GB",validations = {ApiArgValidator.PositiveNumber})
private Long size;
@Parameter(name = ApiConstants.SHRINK_OK, type = CommandType.BOOLEAN, required = false, description = "Verify OK to Shrink")
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 40598d5e12b6..b6b009230a64 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
@@ -57,7 +57,6 @@
import org.apache.cloudstack.engine.subsystem.api.storage.SnapshotInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
-import org.apache.commons.lang3.StringUtils;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.CopyCmdAnswer;
@@ -83,6 +82,7 @@
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
@@ -448,7 +448,9 @@ private CloudStackVolume cloneCloudStackVolumeFromTemplate(StoragePoolVO storage
}
if (!iscsi && qosPolicy != null) {
- attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, qosPolicy);
+ CloudStackVolume cloudStackVolume = createCloudStackVolumeRequestByProtocol(
+ storagePool, details, volumeInfo, qosPolicy);
+ storageStrategy.updateCloudStackVolume(cloudStackVolume);
}
persistQosPolicyDetails(volumeInfo.getId(), qosPolicy);
return cloned;
@@ -1024,72 +1026,93 @@ private void removeTemporarySnapshotCopyRecord(long volumeId) {
}
/**
- * Applies IOPS/QoS on resize. Capacity (LUN/file size) is not updated here.
- * TODO: apply volume size change once size-resize support is merged.
+ * Resizes a volume. Size and QoS from the resize window are applied together by
+ * {@link StorageStrategy#resizeCloudStackVolume}.
*/
@Override
public void resize(DataObject data, AsyncCompletionCallback callback) {
- String errMsg = null;
- String path = null;
+ CreateCmdResult result = null;
try {
- if (!(data instanceof VolumeInfo)) {
- throw new CloudRuntimeException("Invalid DataObjectType ("
- + (data != null ? data.getType() : null) + ") passed to resize");
- }
- VolumeInfo volumeInfo = (VolumeInfo) data;
- path = volumeInfo.getPath();
- applyVolumeQos(volumeInfo);
- } catch (Exception e) {
- errMsg = e.getMessage();
- logger.error("Failed to update IOPS for volume [{}]: {}", data != null ? data.getId() : null, errMsg, e);
- }
+ if (data != null && data.getType() == DataObjectType.VOLUME) {
+ if (data instanceof VolumeInfo) {
+ VolumeQosPolicy policyToApply = null;
+ VolumeInfo volumeInfo = (VolumeInfo) data;
+ Object rawPayload = volumeInfo.getpayload();
+ ResizeVolumePayload payload = (rawPayload instanceof ResizeVolumePayload)
+ ? (ResizeVolumePayload) rawPayload : null;
+ if (payload == null || payload.newSize == null) {
+ throw new CloudRuntimeException("Invalid resize payload for volume " + volumeInfo.getId());
+ }
+ if (volumeInfo.getDataStore() == null) {
+ throw new CloudRuntimeException("Data store not found for volume " + volumeInfo.getId());
+ }
- CreateCmdResult result = new CreateCmdResult(path, new Answer(null, errMsg == null, errMsg));
- result.setResult(errMsg);
- callback.complete(result);
- }
+ StoragePoolVO storagePool = storagePoolDao.findById(volumeInfo.getDataStore().getId());
+ if (storagePool == null) {
+ throw new CloudRuntimeException("Storage pool not found for volume " + volumeInfo.getId());
+ }
+ Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
- private void applyVolumeQos(VolumeInfo volumeInfo) {
- ResizeVolumePayload payload = (ResizeVolumePayload) volumeInfo.getpayload();
- if (payload == null) {
- throw new CloudRuntimeException("Missing resize payload for volume " + volumeInfo.getId());
- }
- VolumeVO volume = volumeDao.findById(volumeInfo.getId());
- if (volume == null || volume.getPoolId() == null) {
- throw new CloudRuntimeException("Unable to resolve volume or storage pool for IOPS update");
- }
- StoragePoolVO storagePool = storagePoolDao.findById(volume.getPoolId());
- if (storagePool == null) {
- throw new CloudRuntimeException("Storage pool not found for volume " + volume.getId());
- }
+ VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId());
+ if (volumeVO == null) {
+ throw new CloudRuntimeException("Volume not found for id " + volumeInfo.getId());
+ }
+ if (payload.newSize < volumeVO.getSize()) {
+ throw new CloudRuntimeException("Unable to shrink volume.");
+ }
- verifySufficientIopsForStoragePool(storagePool, payload.newMinIops, volume.getId());
+ verifySufficientIopsForStoragePool(storagePool, payload.newMinIops, volumeVO.getId());
+ StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
+ VolumeDetailVO qosDetail = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.QOS_POLICY_UUID);
+ String previousUuid = qosDetail != null ? qosDetail.getValue() : null;
+ VolumeQosPolicy qosPolicy = createQosPolicyIfNeeded(storageStrategy, details,
+ payload.newMinIops, payload.newMaxIops, storagePool.getId());
+
+ if (qosPolicy != null && !Objects.equals(previousUuid, qosPolicy.getUuid())) {
+ policyToApply = qosPolicy;
+ } else if (qosPolicy == null && previousUuid != null) {
+ policyToApply = new VolumeQosPolicy();
+ policyToApply.setName(OntapStorageConstants.QOS_POLICY_NONE);
+ }
- Map details = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
- StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(details);
- VolumeDetailVO qosDetail = volumeDetailsDao.findDetail(volume.getId(), OntapStorageConstants.QOS_POLICY_UUID);
- String previousUuid = qosDetail != null ? qosDetail.getValue() : null;
- VolumeQosPolicy qosPolicy = createQosPolicyIfNeeded(storageStrategy, details,
- payload.newMinIops, payload.newMaxIops, volume.getPoolId());
+ try {
+ CloudStackVolume cloudStackVolume = resizeCloudStackVolumeRequest(details, volumeInfo, policyToApply);
+ storageStrategy.resizeCloudStackVolume(cloudStackVolume, payload.newSize);
+ } catch (RuntimeException e) {
+ if (qosPolicy != null && !Objects.equals(previousUuid, qosPolicy.getUuid())) {
+ deleteUnusedQosPolicy(storageStrategy, qosPolicy.getUuid());
+ }
+ throw e;
+ }
+ if (policyToApply != null) {
+ persistQosPolicyDetails(volumeVO.getId(), qosPolicy);
+ deleteUnusedQosPolicy(storageStrategy, previousUuid);
+ }
- if (qosPolicy != null && Objects.equals(previousUuid, qosPolicy.getUuid())) {
- return;
- }
- if (qosPolicy != null) {
- try {
- attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, qosPolicy);
- persistQosPolicyDetails(volume.getId(), qosPolicy);
- } catch (RuntimeException e) {
- deleteUnusedQosPolicy(storageStrategy, qosPolicy.getUuid());
- throw e;
+ volumeVO.setSize(payload.newSize);
+ if (!volumeDao.update(volumeVO.getId(), volumeVO)) {
+ throw new CloudRuntimeException("Failed to update volume " + volumeVO.getId()
+ + " after resizing the ONTAP backing object");
+ }
+ result = new CreateCmdResult(volumeVO.getPath(), new Answer(null, true, null));
+ logger.info("resize: Successfully resized volume [{}] to [{}] bytes", volumeInfo.getId(), payload.newSize);
+ }else{
+ String errorMessage = "Invalid DataObjectType (" + data.getType() + ") passed to resize";
+ result = new CreateCmdResult(null, new Answer(null, false, errorMessage));
+ result.setResult(errorMessage);
+ return;
+ }
+ } else {
+ throw new CloudRuntimeException("Expected a VOLUME DataObject but received " +
+ (data != null ? data.getType() : "null"));
}
- deleteUnusedQosPolicy(storageStrategy, previousUuid);
- return;
- }
- if (previousUuid != null) {
- detachQosPolicy(storageStrategy, storagePool, details, volumeInfo);
- persistQosPolicyDetails(volume.getId(), null);
- deleteUnusedQosPolicy(storageStrategy, previousUuid);
+ } catch (Exception e) {
+ String errMsg = e.getMessage();
+ logger.error("resize: Failed for volume [{}]: {}", data != null ? data.getId() : null, errMsg, e);
+ result = new CreateCmdResult(null, new Answer(null, false, errMsg));
+ result.setResult(errMsg);
+ } finally {
+ callback.complete(result);
}
}
@@ -1104,31 +1127,6 @@ private void deleteUnusedQosPolicy(StorageStrategy storageStrategy, String polic
}
}
- private void attachQosPolicy(StorageStrategy storageStrategy, StoragePoolVO storagePool,
- Map details, VolumeInfo volumeInfo,
- VolumeQosPolicy qosPolicy) {
- CloudStackVolume request = createCloudStackVolumeRequestByProtocol(
- storagePool, details, volumeInfo, qosPolicy);
- if (isIscsi(details)) {
- VolumeDetailVO lunUuid = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.LUN_DOT_UUID);
- if (lunUuid == null || lunUuid.getValue() == null) {
- throw new CloudRuntimeException("LUN UUID is missing for volume " + volumeInfo.getId());
- }
- if (request.getLun() == null) {
- throw new CloudRuntimeException("Missing LUN on QoS update request for volume " + volumeInfo.getId());
- }
- request.getLun().setUuid(lunUuid.getValue());
- }
- storageStrategy.updateCloudStackVolume(request);
- }
-
- private void detachQosPolicy(StorageStrategy storageStrategy, StoragePoolVO storagePool,
- Map details, VolumeInfo volumeInfo) {
- VolumeQosPolicy noPolicy = new VolumeQosPolicy();
- noPolicy.setName(OntapStorageConstants.QOS_POLICY_NONE);
- attachQosPolicy(storageStrategy, storagePool, details, volumeInfo, noPolicy);
- }
-
@Override
public ChapInfo getChapInfo(DataObject dataObject) {
return null;
@@ -1566,9 +1564,59 @@ private boolean isTemplateCachedOnPool(VMTemplateStoragePoolVO templatePoolRef,
return StringUtils.isNotBlank(templatePoolRef.getInstallPath());
}
+ /**
+ * Returns the bytes used on the FlexVolume backing this pool, read directly from ONTAP
+ * ({@code space.used}).
+ *
+ * Fails closed when ONTAP cannot provide trustworthy usage data. Returning zero for an
+ * unreachable or incomplete backend would make capacity checks treat an unknown pool as empty
+ * and could incorrectly authorize a volume grow.
+ *
+ * @throws InvalidParameterValueException if {@code storagePool} is null
+ * @throws CloudRuntimeException if the pool has no FlexVolume UUID in its details, ONTAP
+ * cannot be queried, or used-space data is missing
+ */
@Override
public long getUsedBytes(StoragePool storagePool) {
- return 0;
+ if (storagePool == null) {
+ throw new InvalidParameterValueException("storagePool is null, ensure the pool exists and is fully initialised before querying used bytes");
+ }
+
+ Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(storagePool.getId());
+ String flexVolUuid = poolDetails != null ? poolDetails.get(OntapStorageConstants.VOLUME_UUID) : null;
+
+ if (StringUtils.isBlank(flexVolUuid)) {
+ throw new CloudRuntimeException("FlexVolume UUID not found in pool details for pool " + storagePool.getId());
+ }
+
+ try {
+ StorageStrategy strategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails);
+ Map queryParams = new HashMap<>();
+ queryParams.put(OntapStorageConstants.FIELDS, OntapStorageConstants.SPACE_USED);
+ var flexVol = strategy.getStorageVolume(flexVolUuid, queryParams);
+
+ if (flexVol == null) {
+ throw new CloudRuntimeException(String.format(
+ "FlexVolume [%s] backing pool [%s] was not found on ONTAP",
+ flexVolUuid, storagePool.getId()));
+ }
+ if (flexVol.getSpace() == null) {
+ throw new CloudRuntimeException(String.format(
+ "ONTAP returned no space information for FlexVolume [%s] backing pool [%s]",
+ flexVolUuid, storagePool.getId()));
+ }
+
+ logger.debug("getUsedBytes: FlexVolume [{}] backing pool [{}] reports {} bytes used",
+ flexVolUuid, storagePool.getId(), flexVol.getSpace().getUsed());
+ return flexVol.getSpace().getUsed();
+ } catch (CloudRuntimeException e) {
+ logger.error("getUsedBytes: Failed to get used bytes for pool [{}]", storagePool.getId(), e);
+ throw e;
+ } catch (Exception e) {
+ logger.error("getUsedBytes: Failed to get used bytes for pool [{}]", storagePool.getId(), e);
+ throw new CloudRuntimeException(
+ String.format("Could not read used space for pool [%s]: %s", storagePool.getId(), e.getMessage()), e);
+ }
}
/**
@@ -1949,6 +1997,45 @@ private boolean isIscsi(Map details) {
return ProtocolType.ISCSI.name().equalsIgnoreCase(details.get(OntapStorageConstants.PROTOCOL));
}
+ /**
+ * Resize request. Size is passed separately to {@code resizeCloudStackVolume}.
+ * When QoS changes, iSCSI carries the LUN uuid and policy, and NFS carries the FlexVol uuid and file policy.
+ */
+ private CloudStackVolume resizeCloudStackVolumeRequest(Map details, VolumeInfo volumeInfo,
+ VolumeQosPolicy qosPolicy) {
+ CloudStackVolume resizeRequest = new CloudStackVolume();
+ resizeRequest.setVolumeInfo(volumeInfo);
+ if (qosPolicy == null) {
+ return resizeRequest;
+ }
+ VolumeQosPolicy qosPolicyReference = new VolumeQosPolicy();
+ qosPolicyReference.setName(qosPolicy.getName());
+ qosPolicyReference.setUuid(qosPolicy.getUuid());
+
+ String protocol = details.get(OntapStorageConstants.PROTOCOL);
+ ProtocolType protocolType = ProtocolType.valueOf(protocol);
+ switch (protocolType) {
+ case NFS3:
+ FileInfo fileInfo = new FileInfo();
+ fileInfo.setQosPolicy(qosPolicyReference);
+ resizeRequest.setFile(fileInfo);
+ resizeRequest.setFlexVolumeUuid(details.get(OntapStorageConstants.VOLUME_UUID));
+ return resizeRequest;
+ case ISCSI:
+ VolumeDetailVO lunUuid = volumeDetailsDao.findDetail(volumeInfo.getId(), OntapStorageConstants.LUN_DOT_UUID);
+ if (lunUuid == null || lunUuid.getValue() == null) {
+ throw new CloudRuntimeException("LUN UUID is missing for volume " + volumeInfo.getId());
+ }
+ Lun lun = new Lun();
+ lun.setUuid(lunUuid.getValue());
+ lun.setQosPolicy(qosPolicyReference);
+ resizeRequest.setLun(lun);
+ return resizeRequest;
+ default:
+ throw new CloudRuntimeException("Unsupported protocol " + protocol);
+ }
+ }
+
/**
* Builds the request that creates or updates a volume (LUN for iSCSI, qcow2 file for NFS),
* attaching a QoS policy reference when one is provided.
@@ -2057,6 +2144,7 @@ private CloudStackVolume createCloneLunRequest(StoragePoolVO storagePool, Map queryParams);
@RequestLine("GET /api/storage/volumes")
@Headers({"Authorization: {authHeader}"})
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 5ee9f52273e2..a398e98bd7d7 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
@@ -19,23 +19,25 @@
package org.apache.cloudstack.storage.service;
+import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import com.cloud.utils.StringUtils;
+import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
+import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.FeignClientFactory;
import org.apache.cloudstack.storage.feign.client.AggregateFeignClient;
import org.apache.cloudstack.storage.feign.client.ClusterFeignClient;
+import org.apache.cloudstack.storage.feign.client.EmsFeignClient;
import org.apache.cloudstack.storage.feign.client.JobFeignClient;
import org.apache.cloudstack.storage.feign.client.NASFeignClient;
import org.apache.cloudstack.storage.feign.client.NetworkFeignClient;
import org.apache.cloudstack.storage.feign.client.QosFeignClient;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
-import org.apache.cloudstack.storage.feign.client.EmsFeignClient;
import org.apache.cloudstack.storage.feign.client.SvmFeignClient;
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.Aggregate;
@@ -53,12 +55,12 @@
import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
-import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
+import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -67,7 +69,6 @@
import feign.FeignException;
-import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
/**
* Storage Strategy represents the communication path for all the ONTAP storage options
*
@@ -603,15 +604,38 @@ public void deleteStorageVolume(Volume volume) {
}
/**
- * Gets ONTAP Flex-Volume
- * Eligible only for Unified ONTAP storage
- * throw exception in case of disaggregated ONTAP storage
+ * Gets an ONTAP FlexVolume by UUID.
+ * Eligible only for Unified ONTAP storage.
+ * Throws exception in case of disaggregated ONTAP storage.
*
- * @param volume the volume to retrieve
- * @return the retrieved Volume object
+ * Callers pass ONTAP query parameters when they need a partial record. For example,
+ * {@code fields=space.used} returns only used space instead of the full volume.
+ * A null or empty map returns the default volume representation.
+ *
+ * @param uuid the UUID of the volume to retrieve
+ * @param queryParams ONTAP query parameters applied to {@code GET /storage/volumes/{uuid}};
+ * may be null
+ * @return the retrieved Volume object, or null if not found
*/
- public Volume getStorageVolume(Volume volume) {
- return null;
+ public Volume getStorageVolume(String uuid, Map queryParams) {
+ if (StringUtils.isBlank(uuid)) {
+ throw new CloudRuntimeException("Cannot fetch ONTAP volume: UUID is null or empty");
+ }
+ Map params = queryParams != null ? queryParams : Collections.emptyMap();
+ logger.info("getStorageVolume: Fetching ONTAP volume [{}] with query params {}", uuid, params);
+ String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
+ try {
+ Volume fetchedVolume = volumeFeignClient.getVolumeByUUID(authHeader, uuid, params);
+ logger.info("getStorageVolume: Volume [{}] fetched successfully", uuid);
+ return fetchedVolume;
+ } catch (FeignException e) {
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ logger.warn("getStorageVolume: Volume [{}] not found in ONTAP", uuid);
+ return null;
+ }
+ logger.error("getStorageVolume: Exception while fetching volume [{}]: ", uuid, e);
+ throw new CloudRuntimeException("Failed to fetch volume: " + e.getMessage());
+ }
}
/**
@@ -865,6 +889,11 @@ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool,
*
* Needed after cloning a cached template, because a clone inherits the size of its source
* while the service offering may ask for a larger disk.
+ *
+ * When the request already carries a QoS policy on the LUN or file, that policy is applied
+ * as part of this resize. iSCSI sends one LUN update with the new size and the policy.
+ * NFS grows the file on the host, then updates the file policy. A request with no policy
+ * changes size only.
*/
abstract public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes);
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 79db438d5d03..b0c433ece888 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
@@ -69,6 +69,7 @@
import com.cloud.agent.api.Answer;
import com.cloud.host.HostVO;
+import com.cloud.storage.ResizeVolumePayload;
import com.cloud.storage.Storage;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.dao.VolumeDao;
@@ -262,6 +263,9 @@ public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeI
logger.error("resizeCloudStackVolume: " + errMsg);
throw new CloudRuntimeException(errMsg);
}
+ if (cloudstackVolume.getFile() != null && cloudstackVolume.getFile().getQosPolicy() != null) {
+ updateCloudStackVolume(cloudstackVolume);
+ }
logger.info("resizeCloudStackVolume: Resized volume [{}] to {} bytes", volumeInfo.getUuid(), sizeInBytes);
}
@@ -277,8 +281,15 @@ private Answer resizeVolumeOnKVMHost(DataObject volumeInfo, long sizeInBytes) {
throw new CloudRuntimeException("Storage Pool not found for id: " + volume.getPoolId());
}
- ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
- volume.getSize(), sizeInBytes, false, null);
+ // instanceName is set by VolumeApiServiceImpl.orchestrateResizeVolume() before calling the
+ // driver — it is the VM instance name when attached, or "none" when the volume is detached.
+ ResizeVolumePayload resizePayload = volumeObject.getpayload() instanceof ResizeVolumePayload
+ ? (ResizeVolumePayload) volumeObject.getpayload()
+ : null;
+ String instanceName = resizePayload != null ? resizePayload.instanceName : "none";
+ ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
+ volume.getSize(), sizeInBytes, false, instanceName);
+
EndPoint ep = epSelector.select(volumeInfo);
if (ep == null) {
String errMsg = "No remote endpoint to send ResizeVolumeCommand, check if host is up";
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java
index c783b2914959..6eb4bfc18e24 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
@@ -19,20 +19,24 @@
package org.apache.cloudstack.storage.service;
-import com.cloud.host.HostVO;
-import com.cloud.utils.exception.CloudRuntimeException;
-import feign.FeignException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
+import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Initiator;
-import org.apache.cloudstack.storage.feign.model.Svm;
-import org.apache.cloudstack.storage.feign.model.OntapStorage;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.feign.model.LunMap;
import org.apache.cloudstack.storage.feign.model.LunSpace;
-import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
+import org.apache.cloudstack.storage.feign.model.OntapStorage;
+import org.apache.cloudstack.storage.feign.model.Svm;
+import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.model.AccessGroup;
@@ -41,18 +45,24 @@
import org.apache.cloudstack.storage.utils.OntapStorageConstants;
import org.apache.cloudstack.storage.utils.OntapStorageUtils;
import org.apache.commons.collections.CollectionUtils;
+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.List;
-import java.util.Map;
+
+import com.cloud.host.HostVO;
+import com.cloud.storage.VolumeDetailVO;
+import com.cloud.storage.dao.VolumeDetailsDao;
+import com.cloud.utils.exception.CloudRuntimeException;
+
+import feign.FeignException;
public class UnifiedSANStrategy extends SANStrategy {
private static final Logger logger = LogManager.getLogger(UnifiedSANStrategy.class);
@Inject
private StoragePoolDetailsDao storagePoolDetailsDao;
+ @Inject
+ private VolumeDetailsDao volumeDetailsDao;
public UnifiedSANStrategy(OntapStorage ontapStorage) {
super(ontapStorage);
@@ -191,24 +201,7 @@ private void bestEffortDeleteTemplateCacheLun(String svmName, String lunName, St
@Override
public CloudStackVolume updateCloudStackVolume(CloudStackVolume cloudstackVolume) {
- if (cloudstackVolume == null || cloudstackVolume.getLun() == null
- || cloudstackVolume.getLun().getUuid() == null) {
- throw new CloudRuntimeException("Invalid iSCSI volume QoS update request");
- }
- Lun lunUpdate = new Lun();
- lunUpdate.setQosPolicy(cloudstackVolume.getLun().getQosPolicy());
- try {
- JobResponse response = sanFeignClient.updateLun(
- getAuthHeader(), cloudstackVolume.getLun().getUuid(), lunUpdate);
- pollJobIfPresent(response, "update QoS policy on LUN [" + cloudstackVolume.getLun().getUuid() + "]");
- } catch (FeignException e) {
- throw new CloudRuntimeException("Failed to apply QoS policy to LUN: " + e.getMessage(), e);
- }
- logger.info("Applied QoS policy [{}] to LUN [{}]",
- cloudstackVolume.getLun().getQosPolicy() != null
- ? cloudstackVolume.getLun().getQosPolicy().getName() : null,
- cloudstackVolume.getLun().getUuid());
- return cloudstackVolume;
+ return null;
}
@Override
@@ -272,6 +265,7 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
clonedCloudStackVolume.setLun(lun);
+ clonedCloudStackVolume.setVolumeInfo(cloudstackVolume.getVolumeInfo());
return clonedCloudStackVolume;
} catch (FeignException e) {
logger.error("FeignException occurred while cloning LUN: {}, Status: {}, Exception: {}",
@@ -304,29 +298,57 @@ private void validateCreatedLun(Lun lun, String requestName, String operation) {
*/
@Override
public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes) {
- if (cloudstackVolume == null || cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) {
+ if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null) {
logger.error("resizeCloudStackVolume: Lun resize failed. Invalid request: {}", cloudstackVolume);
throw new CloudRuntimeException("Failed to resize Lun, invalid request");
}
if (sizeInBytes <= 0) {
throw new CloudRuntimeException("Failed to resize Lun, invalid size " + sizeInBytes);
}
+
+ // Resolve LUN UUID from volume details when not pre-populated on the cloudstackVolume
+ // Keep a QoS policy already set on the request so size and QoS go out in one LUN update.
+ VolumeQosPolicy qosPolicy = cloudstackVolume.getLun() != null ? cloudstackVolume.getLun().getQosPolicy() : null;
+ if (cloudstackVolume.getLun() == null || cloudstackVolume.getLun().getUuid() == null) {
+ logger.debug("LUN details not present on cloudstackVolume, resolving UUID from volume details");
+ long volumeId = cloudstackVolume.getVolumeInfo().getId();
+ VolumeDetailVO lunUuidDetail = volumeDetailsDao.findDetail(volumeId, OntapStorageConstants.LUN_DOT_UUID);
+ if (lunUuidDetail == null || StringUtils.isBlank(lunUuidDetail.getValue())) {
+ throw new CloudRuntimeException("LUN UUID not found in volume details for volume " + volumeId);
+ }
+ Lun resolvedLun = new Lun();
+ resolvedLun.setUuid(lunUuidDetail.getValue());
+ cloudstackVolume.setLun(resolvedLun);
+ } else {
+ logger.debug("resizeCloudStackVolume: LUN UUID [{}] already present on cloudstackVolume, skipping volume details lookup", cloudstackVolume.getLun().getUuid());
+ }
+
String lunUuid = cloudstackVolume.getLun().getUuid();
logger.trace("resizeCloudStackVolume: Resizing Lun {} to {} bytes", lunUuid, sizeInBytes);
try {
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
LunSpace lunSpace = new LunSpace();
lunSpace.setSize(sizeInBytes);
- Lun patch = new Lun();
- patch.setSpace(lunSpace);
- JobResponse response = sanFeignClient.updateLun(authHeader, lunUuid, patch);
+ Lun lunPatch = new Lun();
+ lunPatch.setSpace(lunSpace);
+ lunPatch.setQosPolicy(qosPolicy);
+ JobResponse response = sanFeignClient.updateLun(authHeader, lunUuid, lunPatch);
pollJobIfPresent(response, "resize Lun [" + lunUuid + "]");
logger.debug("resizeCloudStackVolume: Lun {} resized to {} bytes", lunUuid, sizeInBytes);
} catch (FeignException e) {
- throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage(), e);
+ logger.error("FeignException occurred while resizing LUN [{}], Status: {}, Exception: {}",
+ lunUuid, e.status(), e.getMessage());
+ if (OntapStorageUtils.isOntapObjectNotFoundError(e)) {
+ throw new CloudRuntimeException(String.format(
+ "LUN [%s] no longer exists on ONTAP; it may have been deleted externally. " +
+ "Verify the LUN is present before retrying the resize.", lunUuid));
+ }
+ throw new CloudRuntimeException(String.format(
+ "Failed to resize LUN [%s]: %s",lunUuid, e.getMessage()));
} catch (Exception e) {
- logger.error("Exception occurred while resizing LUN: {}, Exception: {}", lunUuid, e.getMessage());
- throw new CloudRuntimeException("Failed to resize Lun: " + e.getMessage());
+ logger.error("Exception occurred while resizing LUN [{}]: {}", lunUuid, e.getMessage());
+ throw new CloudRuntimeException(String.format(
+ "Unexpected error while resizing LUN [%s]: %s", lunUuid, e.getMessage()));
}
}
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 7421f9380dd3..2f86b60486ac 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
@@ -59,11 +59,13 @@ public class OntapStorageConstants {
public static final String JOB_SUCCESS = "success";
public static final String TRUE = "true";
- public static final String FALSE = "false";
// Query params
public static final String NAME = "name";
public static final String FIELDS = "fields";
+ /** ONTAP volume field: bytes consumed, {@code space.used}. */
+ public static final String SPACE_USED = "space.used";
+ public static final String CLUSTER_NODE_ASUP_FIELDS = "model,is_all_flash_optimized,is_capacity_optimized,is_perf_optimized";
public static final String INITIATORS = "initiators";
public static final String AGGREGATES = "aggregates";
public static final String STATE = "state";
@@ -90,6 +92,7 @@ public class OntapStorageConstants {
public static final String SEMICOLON = ";";
public static final String COMMA = ",";
public static final String HYPHEN = "-";
+ public static final String DOT = ".";
public static final String VOLUME_PATH_PREFIX = "/vol/";
@@ -150,6 +153,9 @@ public class OntapStorageConstants {
public static final int ONTAP_SFSR_JOB_POLL_INTERVAL_MS = 2000;
public static final int ONTAP_SNAPSHOT_DELETE_JOB_MAX_RETRIES = 30;
public static final int ONTAP_SNAPSHOT_DELETE_JOB_POLL_INTERVAL_MS = 2000;
+ /** Retry settings for FlexVolume create/resize/delete job polling. */
+ public static final int ONTAP_VOLUME_JOB_MAX_RETRIES = 10;
+ public static final int ONTAP_VOLUME_JOB_POLL_INTERVAL_MS = 1000;
public static final int ONTAP_FLEXVOL_JOB_POLL_INTERVAL_MS = 2000;
public static final int ONTAP_FLEXVOL_RESOLVE_MAX_RETRIES = 30;
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 ce7a9b89171a..ff77fe04fa9d 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
@@ -40,6 +40,7 @@
import com.cloud.utils.exception.CloudRuntimeException;
import org.apache.cloudstack.engine.subsystem.api.storage.CopyCommandResult;
import org.apache.cloudstack.engine.subsystem.api.storage.CreateCmdResult;
+import org.apache.cloudstack.engine.subsystem.api.storage.DataObject;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.ObjectInDataStoreStateMachine;
import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStore;
@@ -54,6 +55,8 @@
import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.Igroup;
import org.apache.cloudstack.storage.feign.model.Lun;
+import org.apache.cloudstack.storage.feign.model.VolumeSpace;
+import org.apache.cloudstack.storage.feign.model.Svm;
import org.apache.cloudstack.storage.feign.model.VolumeQosPolicy;
import org.apache.cloudstack.storage.service.StorageStrategy;
import org.apache.cloudstack.storage.service.UnifiedNASStrategy;
@@ -83,6 +86,7 @@
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -99,6 +103,7 @@
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -955,6 +960,7 @@ void testCreateAsync_VolumeClonedFromTemplate_ClonesWithoutGrowing() {
verify(sanStrategy).cloneCloudStackVolume(requestCaptor.capture());
assertEquals("/vol/vol1/cs_tmpl_50", requestCaptor.getValue().getLun().getClone().getSource().getName());
assertEquals("template-lun-uuid", requestCaptor.getValue().getLun().getClone().getSource().getUuid());
+ assertSame(volumeInfo, requestCaptor.getValue().getVolumeInfo());
verify(sanStrategy, never()).createCloudStackVolume(any());
verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
verify(sanStrategy, never()).updateCloudStackVolume(any());
@@ -1010,6 +1016,7 @@ void testCreateAsync_VolumeClonedFromSnapshot_IscsiSuccessWithoutGrow() {
assertNull(lunRequest.getClone().getSource().getUuid());
assertEquals("/vol/vol1/test_volume", lunRequest.getName());
assertEquals("svm1", lunRequest.getSvm().getName());
+ assertSame(volumeInfo, requestCaptor.getValue().getVolumeInfo());
verify(sanStrategy, never()).createCloudStackVolume(any());
verify(sanStrategy, never()).resizeCloudStackVolume(any(), anyLong());
verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.LUN_DOT_UUID), eq("snap-cloned-lun-uuid"), eq(false));
@@ -1537,7 +1544,7 @@ void testCreateAsync_VolumeClonedFromTemplateNFS_RootWithIops_AttachesQosToFile(
when(templatePoolRef.getInstallPath()).thenReturn("template-uuid");
CloudStackVolume cloned = new CloudStackVolume();
- VolumeQosPolicy qosPolicy = qosPolicy("qos-nfs-root-uuid", "cs_100_to_200_iops_svm1");
+ VolumeQosPolicy qosPolicy = lookedUpQosPolicy("qos-nfs-root-uuid", "cs_100_to_200_iops_svm1");
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
stubQosCreateMocks(utilityMock, nasStrategy, cloned, qosPolicy);
@@ -1550,9 +1557,14 @@ void testCreateAsync_VolumeClonedFromTemplateNFS_RootWithIops_AttachesQosToFile(
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
verify(nasStrategy).createVolumeQosPolicy(eq("cs_100_to_200_iops_svm1"), eq(100L), eq(200L));
+ verify(nasStrategy, never()).resizeCloudStackVolume(any(), anyLong());
verify(nasStrategy).updateCloudStackVolume(argThat(request ->
- request.getFile() != null && request.getFile().getQosPolicy() != null
- && "qos-nfs-root-uuid".equals(request.getFile().getQosPolicy().getUuid())));
+ "1".equals(request.getDatastoreId())
+ && "flex-uuid".equals(request.getFlexVolumeUuid())
+ && request.getVolumeInfo() == volumeInfo
+ && request.getFile() != null
+ && isQosPolicyReference(request.getFile().getQosPolicy(),
+ "qos-nfs-root-uuid", "cs_100_to_200_iops_svm1")));
verify(volumeDetailsDao).addDetail(eq(100L), eq(OntapStorageConstants.QOS_POLICY_UUID),
eq("qos-nfs-root-uuid"), eq(false));
}
@@ -1678,34 +1690,30 @@ void testGetUsedIops_SumsMinIopsIncludingCreatingVolumes() {
@Test
void testResize_IopsUpdate_AttachesQosPolicy() {
long currentSize = 4L * 1024 * 1024 * 1024;
- when(volumeInfo.getId()).thenReturn(100L);
+ stubResizeVolumeRecord(currentSize);
when(volumeInfo.getpayload()).thenReturn(
new ResizeVolumePayload(currentSize, 0L, 5000L, null, false, null, null, true));
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
- when(volumeVO.getPoolId()).thenReturn(1L);
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
when(storagePool.getCapacityIops()).thenReturn(null);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
- VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false));
- VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_0_to_5000_iops_svm1");
- CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+ VolumeQosPolicy qosPolicy = lookedUpQosPolicy("qos-uuid", "cs_0_to_5000_iops_svm1");
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
- when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
+ stubQosCreateMocks(utilityMock, sanStrategy, iscsiCloudStackVolume(), qosPolicy);
driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- verify(sanStrategy).updateCloudStackVolume(argThat(request ->
- request.getLun() != null && "lun-uuid-123".equals(request.getLun().getUuid())));
+ verify(sanStrategy).resizeCloudStackVolume(argThat(request ->
+ request.getLun() != null
+ && "lun-uuid-123".equals(request.getLun().getUuid())
+ && isQosPolicyReference(request.getLun().getQosPolicy(), "qos-uuid", "cs_0_to_5000_iops_svm1")),
+ eq(currentSize));
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
verify(volumeDetailsDao).addDetail(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
}
}
@@ -1715,31 +1723,28 @@ void testResize_IopsUpdate_NfsAttachesQosPolicy() {
long currentSize = 4L * 1024 * 1024 * 1024;
storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name());
storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flex-uuid");
- when(volumeInfo.getId()).thenReturn(100L);
+ stubResizeVolumeRecord(currentSize);
when(volumeInfo.getpayload()).thenReturn(
new ResizeVolumePayload(currentSize, 100L, 200L, null, false, null, null, true));
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
- when(volumeVO.getPoolId()).thenReturn(1L);
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
when(storagePool.getCapacityIops()).thenReturn(null);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(null);
- VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
- CloudStackVolume cloudStackVolume = nfsCloudStackVolume();
+ VolumeQosPolicy qosPolicy = lookedUpQosPolicy("qos-uuid", "cs_100_to_200_iops_svm1");
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- stubQosCreateMocks(utilityMock, nasStrategy, cloudStackVolume, qosPolicy);
- when(nasStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
+ stubQosCreateMocks(utilityMock, nasStrategy, nfsCloudStackVolume(), qosPolicy);
driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- verify(nasStrategy).updateCloudStackVolume(any());
+ verify(nasStrategy).resizeCloudStackVolume(argThat(request ->
+ "flex-uuid".equals(request.getFlexVolumeUuid())
+ && request.getFile() != null
+ && isQosPolicyReference(request.getFile().getQosPolicy(), "qos-uuid", "cs_100_to_200_iops_svm1")),
+ eq(currentSize));
+ verify(nasStrategy, never()).updateCloudStackVolume(any());
verify(volumeDetailsDao).addDetail(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
}
}
@@ -1747,30 +1752,24 @@ void testResize_IopsUpdate_NfsAttachesQosPolicy() {
@Test
void testResize_SameQosPolicy_SkipsAttach() {
long currentSize = 4L * 1024 * 1024 * 1024;
- when(volumeInfo.getId()).thenReturn(100L);
+ stubResizeVolumeRecord(currentSize);
when(volumeInfo.getpayload()).thenReturn(
new ResizeVolumePayload(currentSize, 0L, 3333L, null, false, null, null, true));
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
- when(volumeVO.getPoolId()).thenReturn(1L);
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
when(storagePool.getCapacityIops()).thenReturn(null);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false));
VolumeQosPolicy qosPolicy = qosPolicy("qos-uuid", "cs_0_to_3333_iops_svm1");
- CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
- stubQosCreateMocks(utilityMock, sanStrategy, cloudStackVolume, qosPolicy);
+ stubQosCreateMocks(utilityMock, sanStrategy, iscsiCloudStackVolume(), qosPolicy);
driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
+ verify(sanStrategy).resizeCloudStackVolume(argThat(request -> request.getLun() == null), eq(currentSize));
verify(sanStrategy, never()).updateCloudStackVolume(any());
}
}
@@ -1778,35 +1777,29 @@ void testResize_SameQosPolicy_SkipsAttach() {
@Test
void testResize_ClearIops_DetachesQosPolicy() {
long currentSize = 4L * 1024 * 1024 * 1024;
- when(volumeInfo.getId()).thenReturn(100L);
+ stubResizeVolumeRecord(currentSize);
when(volumeInfo.getpayload()).thenReturn(
new ResizeVolumePayload(currentSize, 0L, 0L, null, false, null, null, true));
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
- when(volumeVO.getPoolId()).thenReturn(1L);
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
when(storagePool.getCapacityIops()).thenReturn(null);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
- VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "qos-uuid", false));
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false));
- CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class, CALLS_REAL_METHODS)) {
utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(any()))
.thenReturn(sanStrategy);
- when(sanStrategy.updateCloudStackVolume(any())).thenReturn(cloudStackVolume);
driver.resize(volumeInfo, createCallback);
ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CreateCmdResult.class);
verify(createCallback).complete(resultCaptor.capture());
assertTrue(resultCaptor.getValue().isSuccess());
- verify(sanStrategy).updateCloudStackVolume(argThat(request ->
- request.getLun() != null && request.getLun().getQosPolicy() != null
- && OntapStorageConstants.QOS_POLICY_NONE.equals(request.getLun().getQosPolicy().getName())));
+ verify(sanStrategy).resizeCloudStackVolume(argThat(request ->
+ request.getLun() != null
+ && isQosPolicyReference(request.getLun().getQosPolicy(), null, OntapStorageConstants.QOS_POLICY_NONE)),
+ eq(currentSize));
+ verify(sanStrategy, never()).updateCloudStackVolume(any());
verify(sanStrategy).deleteVolumeQosPolicy("qos-uuid");
}
}
@@ -1814,27 +1807,19 @@ void testResize_ClearIops_DetachesQosPolicy() {
@Test
void testResize_SwitchIops_OldPolicyDeleteFails_StillSucceeds() {
long currentSize = 4L * 1024 * 1024 * 1024;
- when(volumeInfo.getId()).thenReturn(100L);
+ stubResizeVolumeRecord(currentSize);
when(volumeInfo.getpayload()).thenReturn(
new ResizeVolumePayload(currentSize, 0L, 5000L, null, false, null, null, true));
- when(volumeDao.findById(100L)).thenReturn(volumeVO);
- when(volumeVO.getId()).thenReturn(100L);
- when(volumeVO.getPoolId()).thenReturn(1L);
- when(storagePoolDao.findById(1L)).thenReturn(storagePool);
- when(storagePool.getId()).thenReturn(1L);
when(storagePool.getCapacityIops()).thenReturn(null);
- when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails);
- VolumeDetailVO qosDetail = new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "old-qos-uuid", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID)).thenReturn(qosDetail);
- VolumeDetailVO lunUuidDetail = new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false);
- when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID)).thenReturn(lunUuidDetail);
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.QOS_POLICY_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.QOS_POLICY_UUID, "old-qos-uuid", false));
+ when(volumeDetailsDao.findDetail(100L, OntapStorageConstants.LUN_DOT_UUID))
+ .thenReturn(new VolumeDetailVO(100L, OntapStorageConstants.LUN_DOT_UUID, "lun-uuid-123", false));
- VolumeQosPolicy qosPolicy = qosPolicy("new-qos-uuid", "cs_0_to_5000_iops_svm1");
- CloudStackVolume cloudStackVolume = iscsiCloudStackVolume();
+ VolumeQosPolicy qosPolicy = lookedUpQosPolicy("new-qos-uuid", "cs_0_to_5000_iops_svm1");
try (MockedStatic