From cd08c006a215fcfcf977410d6040a17bc07089e9 Mon Sep 17 00:00:00 2001 From: "Jain, Rajiv" Date: Tue, 20 Jan 2026 20:15:03 +0530 Subject: [PATCH 1/4] CSTACKEX-18: Cloudstack snapshot workflow for NFS3 protocol --- .../driver/OntapPrimaryDatastoreDriver.java | 168 ++++++++++++++++++ .../storage/feign/client/NASFeignClient.java | 7 + .../storage/feign/model/FileClone.java | 32 ++++ .../storage/feign/model/VolumeConcise.java | 24 +++ .../storage/service/StorageStrategy.java | 18 +- .../storage/service/UnifiedNASStrategy.java | 134 +++++++++----- .../storage/service/UnifiedSANStrategy.java | 7 +- .../service/model/CloudStackVolume.java | 28 +++ .../cloudstack/storage/utils/Constants.java | 7 +- 9 files changed, 378 insertions(+), 47 deletions(-) create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java create mode 100644 plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.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 5e79aa2298da..90d075594498 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java @@ -27,6 +27,11 @@ import com.cloud.storage.Storage; import com.cloud.storage.StoragePool; import com.cloud.storage.Volume; +import com.cloud.storage.VolumeVO; +import com.cloud.storage.dao.SnapshotDetailsDao; +import com.cloud.storage.dao.SnapshotDetailsVO; +import com.cloud.storage.dao.VolumeDao; +import com.cloud.storage.dao.VolumeDetailsDao; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; import org.apache.cloudstack.engine.subsystem.api.storage.ChapInfo; @@ -41,14 +46,18 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo; import org.apache.cloudstack.framework.async.AsyncCompletionCallback; import org.apache.cloudstack.storage.command.CommandResult; +import org.apache.cloudstack.storage.command.CreateObjectAnswer; 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.service.StorageStrategy; 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.Constants; import org.apache.cloudstack.storage.utils.Utility; +import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -62,6 +71,9 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver { @Inject private StoragePoolDetailsDao storagePoolDetailsDao; @Inject private PrimaryDataStoreDao storagePoolDao; + @Inject private VolumeDao volumeDao; + @Inject private VolumeDetailsDao volumeDetailsDao; + @Inject private SnapshotDetailsDao snapshotDetailsDao; @Override public Map getCapabilities() { @@ -227,6 +239,79 @@ public long getUsedIops(StoragePool storagePool) { @Override public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) { + CreateCmdResult result; + + try { + VolumeInfo volumeInfo = snapshot.getBaseVolume(); + + VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId()); + if(volumeVO == null) { + throw new CloudRuntimeException("takeSnapshot: VolumeVO not found for id: " + volumeInfo.getId()); + } + + /** we are keeping file path at volumeVO.getPath() */ + + StoragePoolVO storagePool = storagePoolDao.findById(volumeVO.getPoolId()); + if(storagePool == null) { + s_logger.error("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId()); + throw new CloudRuntimeException("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId()); + } + Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId()); + StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(poolDetails); + + CloudStackVolume cloudStackVolumeRequest = getCloudStackVolumeRequestByProtocol(poolDetails, volumeVO.getPath()); + CloudStackVolume cloudStackVolume = storageStrategy.getCloudStackVolume(cloudStackVolumeRequest); + if (cloudStackVolume == null || cloudStackVolume.getFile() == null) { + throw new CloudRuntimeException("takeSnapshot: Failed to get source file to take snapshot"); + } + long capacityBytes = storagePool.getCapacityBytes(); + + long usedBytes = getUsedBytes(storagePool); + long fileSize = cloudStackVolume.getFile().getSize(); + + usedBytes += fileSize; + + if (usedBytes > capacityBytes) { + throw new CloudRuntimeException("Insufficient space remains in this primary storage to take a snapshot"); + } + + storagePool.setUsedBytes(usedBytes); + + SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO)snapshot.getTO(); + + String fileSnapshotName = volumeInfo.getName() + "-" + snapshot.getUuid(); + + int maxSnapshotNameLength = 64; + int trimRequired = fileSnapshotName.length() - maxSnapshotNameLength; + + if (trimRequired > 0) { + fileSnapshotName = StringUtils.left(volumeInfo.getName(), (volumeInfo.getName().length() - trimRequired)) + "-" + snapshot.getUuid(); + } + + CloudStackVolume snapCloudStackVolumeRequest = snapshotCloudStackVolumeRequestByProtocol(poolDetails, volumeVO.getPath(), fileSnapshotName); + CloudStackVolume cloneCloudStackVolume = storageStrategy.snapshotCloudStackVolume(snapCloudStackVolumeRequest); + + updateSnapshotDetails(snapshot.getId(), volumeInfo.getId(), poolDetails.get(Constants.VOLUME_UUID), cloneCloudStackVolume.getFile().getPath(), volumeVO.getPoolId(), fileSize); + + snapshotObjectTo.setPath(Constants.ONTAP_SNAP_ID +"="+cloneCloudStackVolume.getFile().getPath()); + + /** Update size for the storage-pool including snapshot size */ + storagePoolDao.update(volumeVO.getPoolId(), storagePool); + + CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo); + + result = new CreateCmdResult(null, createObjectAnswer); + + result.setResult(null); + } + catch (Exception ex) { + s_logger.error("takeSnapshot: Failed due to ", ex); + result = new CreateCmdResult(null, new CreateObjectAnswer(ex.toString())); + + result.setResult(ex.toString()); + } + + callback.complete(result); } @Override @@ -293,4 +378,87 @@ public boolean isStorageSupportHA(Storage.StoragePoolType type) { public void detachVolumeFromAllStorageNodes(Volume volume) { } + + + private CloudStackVolume getCloudStackVolumeRequestByProtocol(Map details, String filePath) { + CloudStackVolume cloudStackVolumeRequest = null; + ProtocolType protocolType = null; + String protocol = null; + + try { + protocol = details.get(Constants.PROTOCOL); + protocolType = ProtocolType.valueOf(protocol); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("getCloudStackVolumeRequestByProtocol: Protocol: "+ protocol +" is not valid"); + } + switch (protocolType) { + case NFS3: + cloudStackVolumeRequest = new CloudStackVolume(); + FileInfo fileInfo = new FileInfo(); + fileInfo.setPath(filePath); + cloudStackVolumeRequest.setFile(fileInfo); + String volumeUuid = details.get(Constants.VOLUME_UUID); + cloudStackVolumeRequest.setFlexVolumeUuid(volumeUuid); + break; + default: + throw new CloudRuntimeException("createCloudStackVolumeRequestByProtocol: Unsupported protocol " + protocol); + } + return cloudStackVolumeRequest; + } + +private CloudStackVolume snapshotCloudStackVolumeRequestByProtocol(Map details, + String sourcePath, + String destinationPath) { + CloudStackVolume cloudStackVolumeRequest = null; + ProtocolType protocolType = null; + String protocol = null; + + try { + protocol = details.get(Constants.PROTOCOL); + protocolType = ProtocolType.valueOf(protocol); + } catch (IllegalArgumentException e) { + throw new CloudRuntimeException("getCloudStackVolumeRequestByProtocol: Protocol: "+ protocol +" is not valid"); + } + switch (protocolType) { + case NFS3: + cloudStackVolumeRequest = new CloudStackVolume(); + FileInfo fileInfo = new FileInfo(); + fileInfo.setPath(sourcePath); + cloudStackVolumeRequest.setFile(fileInfo); + String volumeUuid = details.get(Constants.VOLUME_UUID); + cloudStackVolumeRequest.setFlexVolumeUuid(volumeUuid); + cloudStackVolumeRequest.setDestinationPath(destinationPath); + break; + default: + throw new CloudRuntimeException("createCloudStackVolumeRequestByProtocol: Unsupported protocol " + protocol); + + } + return cloudStackVolumeRequest; +} + + /** + * + * @param csSnapshotId: generated snapshot id from cloudstack + * @param csVolumeId: Source CS volume id + * @param ontapVolumeUuid: storage flexvolume id + * @param ontapNewSnapshot: generated snapshot id from ONTAP + * @param storagePoolId: primary storage pool id + * @param ontapSnapSize: Size of snapshot CS volume(LUN/file) + */ + private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String ontapVolumeUuid, String ontapNewSnapshot, long storagePoolId, long ontapSnapSize) { + SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.SRC_CS_VOLUME_ID, String.valueOf(csVolumeId), false); + snapshotDetailsDao.persist(snapshotDetail); + + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.BASE_ONTAP_FV_ID, String.valueOf(ontapVolumeUuid), false); + snapshotDetailsDao.persist(snapshotDetail); + + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_ID, String.valueOf(ontapNewSnapshot), false); + snapshotDetailsDao.persist(snapshotDetail); + + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.PRIMARY_POOL_ID, String.valueOf(storagePoolId), false); + snapshotDetailsDao.persist(snapshotDetail); + + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_SIZE, String.valueOf(ontapSnapSize), false); + snapshotDetailsDao.persist(snapshotDetail); +} } 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 f48f83dc28de..8d4df6d8c4f1 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.FileClone; 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,11 @@ void createFile(@Param("authHeader") String authHeader, @Param("path") String filePath, FileInfo file); + @RequestLine("POST /api/storage/volumes/{volumeUuid}/files/{path}") + @Headers({"Authorization: {authHeader}"}) + JobResponse cloneFile(@Param("authHeader") String authHeader, + FileClone fileClone); + // 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/model/FileClone.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java new file mode 100644 index 000000000000..836538baab96 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java @@ -0,0 +1,32 @@ +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; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class FileClone { + @JsonProperty("source_path") + private String sourcePath; + @JsonProperty("destination_path") + private String destinationPath; + @JsonProperty("volume") + private VolumeConcise volume; + public VolumeConcise getVolume() { + return volume; + } + public void setVolume(VolumeConcise 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) {} +} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java new file mode 100644 index 000000000000..e50641b21d95 --- /dev/null +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java @@ -0,0 +1,24 @@ +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; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class VolumeConcise { + @JsonProperty("uuid") + private String uuid; + @JsonProperty("name") + private String name; + public String getUuid() { + return uuid; + } + public void setUuid(String uuid) { + this.uuid = uuid; + } + public String getName() { + return name; + } + public void setName(String name) {} +} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java index 822e09851f39..88d21ec28ac1 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 @@ -393,8 +393,6 @@ public String getStoragePath() { return targetIqn; } - - /** * Get the network ip interface * @@ -484,7 +482,19 @@ public String getNetworkInterface() { * @param cloudstackVolume the CloudStack volume to retrieve * @return the retrieved CloudStackVolume object */ - abstract CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume); + public abstract CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume); + + /** + * Method encapsulates the behavior based on the opted protocol in subclasses. + * it is going to mimic + * snapshotLun for iSCSI, FC protocols + * snapshotFile for NFS3.0 and NFS4.1 protocols + * + * + * @param cloudstackVolume the source CloudStack volume + * @return the retrieved snapshot CloudStackVolume object + */ + public abstract CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolume); /** * Method encapsulates the behavior based on the opted protocol in subclasses @@ -547,7 +557,7 @@ public String getNetworkInterface() { */ abstract void disableLogicalAccess(Map values); - private Boolean jobPollForSuccess(String jobUUID) { + public Boolean jobPollForSuccess(String jobUUID) { //Create URI for GET Job API int jobRetryCount = 0; Job jobResp = null; diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java index b35bedf2ef3c..c47d50921924 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java @@ -29,21 +29,21 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; -import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.storage.command.CreateObjectCommand; -import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.feign.FeignClientFactory; import org.apache.cloudstack.storage.feign.client.JobFeignClient; import org.apache.cloudstack.storage.feign.client.NASFeignClient; import org.apache.cloudstack.storage.feign.client.VolumeFeignClient; import org.apache.cloudstack.storage.feign.model.ExportPolicy; import org.apache.cloudstack.storage.feign.model.ExportRule; +import org.apache.cloudstack.storage.feign.model.FileClone; import org.apache.cloudstack.storage.feign.model.FileInfo; 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.Volume; +import org.apache.cloudstack.storage.feign.model.VolumeConcise; 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; @@ -68,7 +68,6 @@ public class UnifiedNASStrategy extends NASStrategy { private final JobFeignClient jobFeignClient; @Inject private VolumeDao volumeDao; @Inject private EndPointSelector epSelector; - @Inject private StoragePoolDetailsDao storagePoolDetailsDao; public UnifiedNASStrategy(OntapStorage ontapStorage) { super(ontapStorage); @@ -115,9 +114,71 @@ void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume) { - //TODO - return null; + public CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolumeArg) { + s_logger.info("getCloudStackVolume: Get cloudstack volume " + cloudstackVolumeArg); + CloudStackVolume cloudStackVolume = null; + FileInfo fileInfo = getFile(cloudstackVolumeArg.getFlexVolumeUuid(),cloudstackVolumeArg.getFile().getPath()); + + if(fileInfo != null){ + cloudStackVolume = new CloudStackVolume(); + cloudStackVolume.setFlexVolumeUuid(cloudstackVolumeArg.getFlexVolumeUuid()); + cloudStackVolume.setFile(fileInfo); + } + + return cloudStackVolume; + } + + @Override + public CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolumeArg) { + s_logger.info("snapshotCloudStackVolume: Get cloudstack volume " + cloudstackVolumeArg); + CloudStackVolume cloudStackVolume = null; + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + JobResponse jobResponse = null; + + FileClone fileClone = new FileClone(); + VolumeConcise volumeConcise = new VolumeConcise(); + volumeConcise.setUuid(cloudstackVolumeArg.getFlexVolumeUuid()); + fileClone.setVolume(volumeConcise); + + fileClone.setSourcePath(cloudstackVolumeArg.getFile().getPath()); + fileClone.setDestinationPath(cloudstackVolumeArg.getDestinationPath()); + + try { + /** Clone file call to storage */ + jobResponse = nasFeignClient.cloneFile(authHeader, fileClone); + if (jobResponse == null || jobResponse.getJob() == null) { + throw new CloudRuntimeException("Failed to initiate file clone" + cloudstackVolumeArg.getFile().getPath()); + } + String jobUUID = jobResponse.getJob().getUuid(); + + /** Create URI for GET Job API */ + Boolean jobSucceeded = jobPollForSuccess(jobUUID); + if (!jobSucceeded) { + s_logger.error("File clone failed: " + cloudstackVolumeArg.getFile().getPath()); + throw new CloudRuntimeException("File clone failed: " + cloudstackVolumeArg.getFile().getPath()); + } + s_logger.info("File clone job completed successfully for file: " + cloudstackVolumeArg.getFile().getPath()); + + } catch (FeignException e) { + s_logger.error("Failed to clone file response: " + cloudstackVolumeArg.getFile().getPath(), e); + throw new CloudRuntimeException("File not found: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception to get file: {}", cloudstackVolumeArg.getFile().getPath(), e); + throw new CloudRuntimeException("Failed to get the file: " + e.getMessage()); + } + + FileInfo clonedFileInfo = null; + try { + /** Get cloned file call from storage */ + clonedFileInfo = getFile(cloudstackVolumeArg.getFlexVolumeUuid(), cloudstackVolumeArg.getDestinationPath()); + } catch (Exception e) { + s_logger.error("Exception to get cloned file: {}", cloudstackVolumeArg.getDestinationPath(), e); + throw new CloudRuntimeException("Failed to get the cloned file: " + e.getMessage()); + } + cloudStackVolume = new CloudStackVolume(); + cloudStackVolume.setFlexVolumeUuid(cloudstackVolumeArg.getFlexVolumeUuid()); + cloudStackVolume.setFile(clonedFileInfo); + return cloudStackVolume; } @Override @@ -135,9 +196,6 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.info("ExportPolicy created: {}, now attaching this policy to storage pool volume", createdPolicy.getName()); // attach export policy to volume of storage pool assignExportPolicyToVolume(volumeUUID,createdPolicy.getName()); - // save the export policy details in storage pool details - storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_ID, String.valueOf(createdPolicy.getId()), true); - storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_NAME, createdPolicy.getName(), true); s_logger.info("Successfully assigned exportPolicy {} to volume {}", policyRequest.getName(), volumeName); accessGroup.setPolicy(policyRequest); return accessGroup; @@ -149,36 +207,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { @Override public void deleteAccessGroup(AccessGroup accessGroup) { - s_logger.info("deleteAccessGroup: Deleting export policy"); - - if (accessGroup == null) { - throw new CloudRuntimeException("deleteAccessGroup: Invalid accessGroup object - accessGroup is null"); - } - - // Get PrimaryDataStoreInfo from accessGroup - PrimaryDataStoreInfo primaryDataStoreInfo = accessGroup.getPrimaryDataStoreInfo(); - if (primaryDataStoreInfo == null) { - throw new CloudRuntimeException("deleteAccessGroup: PrimaryDataStoreInfo is null in accessGroup"); - } - s_logger.info("deleteAccessGroup: Deleting export policy for the storage pool {}", primaryDataStoreInfo.getName()); - try { - String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); - String svmName = storage.getSvmName(); - // Determine export policy attached to the storage pool - String exportPolicyName = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_NAME); - String exportPolicyId = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_ID); - - try { - nasFeignClient.deleteExportPolicyById(authHeader,exportPolicyId); - s_logger.info("deleteAccessGroup: Successfully deleted export policy '{}'", exportPolicyName); - } catch (Exception e) { - s_logger.error("deleteAccessGroup: Failed to delete export policy. Exception: {}", e.getMessage(), e); - throw new CloudRuntimeException("Failed to delete export policy: " + e.getMessage(), e); - } - } catch (Exception e) { - s_logger.error("deleteAccessGroup: Failed to delete export policy. Exception: {}", e.getMessage(), e); - throw new CloudRuntimeException("Failed to delete export policy: " + e.getMessage(), e); - } + //TODO } @Override @@ -375,10 +404,13 @@ private boolean updateFile(String volumeUuid, String filePath, FileInfo fileInfo } } + private String generateExportPolicyName(String svmName, String volumeName){ + return Constants.EXPORT + Constants.HYPHEN + svmName + Constants.HYPHEN + volumeName; + } private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String svmName , String volumeName){ - String exportPolicyName = Utility.generateExportPolicyName(svmName,volumeName); + String exportPolicyName = generateExportPolicyName(svmName,volumeName); ExportPolicy exportPolicy = new ExportPolicy(); List rules = new ArrayList<>(); @@ -460,4 +492,26 @@ private Answer createVolumeOnKVMHost(DataObject volumeInfo) { return new Answer(null, false, e.toString()); } } + + private FileInfo getFile(String volumeUuid, String filePath) { + s_logger.info("Get File: {} for volume: {}", filePath, volumeUuid); + + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + OntapResponse fileResponse = null; + try { + fileResponse = nasFeignClient.getFileResponse(authHeader, volumeUuid, filePath); + if (fileResponse == null || fileResponse.getRecords().isEmpty()) { + throw new CloudRuntimeException("File " + filePath + " not not found on ONTAP. " + + "Received successful response but file does not exist."); + } + } catch (FeignException e) { + s_logger.error("Failed to get file response: " + filePath, e); + throw new CloudRuntimeException("File not found: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Exception to get file: {}", filePath, e); + throw new CloudRuntimeException("Failed to get the file: " + e.getMessage()); + } + s_logger.info("File retrieved successfully with name {}", filePath); + return fileResponse.getRecords().get(0); + } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java index 7b5372c69bdd..f738e3e5d2da 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 @@ -104,11 +104,16 @@ void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) { } @Override - CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume) { + public CloudStackVolume getCloudStackVolume(CloudStackVolume cloudstackVolume) { //TODO return null; } + @Override + public CloudStackVolume snapshotCloudStackVolume(CloudStackVolume cloudstackVolume) { + return null; + } + @Override public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.info("createAccessGroup : Create Igroup"); 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 6c51e4630800..0153f7f6031a 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 @@ -25,9 +25,27 @@ public class CloudStackVolume { + /** + * Filed used for request: + * a. snapshot worflows will get source file details from it. + */ private FileInfo file; + /** + * Filed used for request: + * a. snapshot worflows will get source LUN details from it. + */ private Lun lun; private String datastoreId; + /** + * FlexVolume UUID on which this cloudstack volume is created. + * a. Field is eligible for unified storage only. + * b. It will be null for the disaggregated storage. + */ + private String flexVolumeUuid; + /** + * Field serves for snapshot workflows + */ + private String destinationPath; private DataObject volumeInfo; // This is needed as we need DataObject to be passed to agent to create volume public FileInfo getFile() { return file; @@ -37,6 +55,10 @@ public void setFile(FileInfo file) { this.file = file; } + public String getDestinationPath() { return this.destinationPath; } + + public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; } + public Lun getLun() { return lun; } @@ -56,4 +78,10 @@ public DataObject getVolumeInfo() { public void setVolumeInfo(DataObject volumeInfo) { this.volumeInfo = volumeInfo; } + public String getFlexVolumeUuid() { + return flexVolumeUuid; + } + public void setFlexVolumeUuid(String flexVolumeUuid) { + this.flexVolumeUuid = flexVolumeUuid; + } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java index 23425aa6b797..e73d726d97ea 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java @@ -33,8 +33,6 @@ public class Constants { public static final String MANAGEMENT_LIF = "managementLIF"; public static final String VOLUME_NAME = "volumeName"; public static final String VOLUME_UUID = "volumeUUID"; - public static final String EXPORT_POLICY_ID = "exportPolicyId"; - public static final String EXPORT_POLICY_NAME = "exportPolicyName"; public static final String IS_DISAGGREGATED = "isDisaggregated"; public static final String RUNNING = "running"; public static final String EXPORT = "export"; @@ -84,4 +82,9 @@ public class Constants { public static final String IGROUP_DOT_UUID = "igroup.uuid"; public static final String UNDERSCORE = "_"; public static final String CS = "cs"; + public static final String SRC_CS_VOLUME_ID = "src_cs_volume_id"; + public static final String BASE_ONTAP_FV_ID = "base_ontap_fv_id"; + public static final String ONTAP_SNAP_ID = "ontap_snap_id"; + public static final String PRIMARY_POOL_ID = "primary_pool_id"; + public static final String ONTAP_SNAP_SIZE = "ontap_snap_size"; } From 7826d9817c1d988fa4709bcc9db19065e461c635 Mon Sep 17 00:00:00 2001 From: "Jain, Rajiv" Date: Tue, 20 Jan 2026 20:23:04 +0530 Subject: [PATCH 2/4] CSTACKEX-18: missed to add copyright in the new files --- .../storage/feign/model/FileClone.java | 19 +++++++++++++++++++ .../storage/feign/model/VolumeConcise.java | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java index 836538baab96..a117ec6e6a0b 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileClone.java @@ -1,3 +1,22 @@ +/* + * 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; diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java index e50641b21d95..eaa5b2ed2ae9 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/VolumeConcise.java @@ -1,3 +1,22 @@ +/* + * 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; From 01746361f3745cd15f8ba238749a9436c99f38fa Mon Sep 17 00:00:00 2001 From: "Jain, Rajiv" Date: Tue, 20 Jan 2026 20:40:14 +0530 Subject: [PATCH 3/4] CSTACKEX-18: pulling the reversed code of delete access group --- .../storage/service/UnifiedNASStrategy.java | 42 ++++++++++++++++--- .../cloudstack/storage/utils/Constants.java | 2 + 2 files changed, 39 insertions(+), 5 deletions(-) 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 c47d50921924..3a6c0998c087 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedNASStrategy.java @@ -29,7 +29,9 @@ import org.apache.cloudstack.engine.subsystem.api.storage.DataObject; import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint; import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector; +import org.apache.cloudstack.engine.subsystem.api.storage.PrimaryDataStoreInfo; import org.apache.cloudstack.storage.command.CreateObjectCommand; +import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.feign.FeignClientFactory; import org.apache.cloudstack.storage.feign.client.JobFeignClient; import org.apache.cloudstack.storage.feign.client.NASFeignClient; @@ -68,6 +70,7 @@ public class UnifiedNASStrategy extends NASStrategy { private final JobFeignClient jobFeignClient; @Inject private VolumeDao volumeDao; @Inject private EndPointSelector epSelector; + @Inject private StoragePoolDetailsDao storagePoolDetailsDao; public UnifiedNASStrategy(OntapStorage ontapStorage) { super(ontapStorage); @@ -196,6 +199,9 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { s_logger.info("ExportPolicy created: {}, now attaching this policy to storage pool volume", createdPolicy.getName()); // attach export policy to volume of storage pool assignExportPolicyToVolume(volumeUUID,createdPolicy.getName()); + // save the export policy details in storage pool details + storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_ID, String.valueOf(createdPolicy.getId()), true); + storagePoolDetailsDao.addDetail(accessGroup.getPrimaryDataStoreInfo().getId(), Constants.EXPORT_POLICY_NAME, createdPolicy.getName(), true); s_logger.info("Successfully assigned exportPolicy {} to volume {}", policyRequest.getName(), volumeName); accessGroup.setPolicy(policyRequest); return accessGroup; @@ -207,7 +213,36 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) { @Override public void deleteAccessGroup(AccessGroup accessGroup) { - //TODO + s_logger.info("deleteAccessGroup: Deleting export policy"); + + if (accessGroup == null) { + throw new CloudRuntimeException("deleteAccessGroup: Invalid accessGroup object - accessGroup is null"); + } + + // Get PrimaryDataStoreInfo from accessGroup + PrimaryDataStoreInfo primaryDataStoreInfo = accessGroup.getPrimaryDataStoreInfo(); + if (primaryDataStoreInfo == null) { + throw new CloudRuntimeException("deleteAccessGroup: PrimaryDataStoreInfo is null in accessGroup"); + } + s_logger.info("deleteAccessGroup: Deleting export policy for the storage pool {}", primaryDataStoreInfo.getName()); + try { + String authHeader = Utility.generateAuthHeader(storage.getUsername(), storage.getPassword()); + String svmName = storage.getSvmName(); + // Determine export policy attached to the storage pool + String exportPolicyName = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_NAME); + String exportPolicyId = primaryDataStoreInfo.getDetails().get(Constants.EXPORT_POLICY_ID); + + try { + nasFeignClient.deleteExportPolicyById(authHeader,exportPolicyId); + s_logger.info("deleteAccessGroup: Successfully deleted export policy '{}'", exportPolicyName); + } catch (Exception e) { + s_logger.error("deleteAccessGroup: Failed to delete export policy. Exception: {}", e.getMessage(), e); + throw new CloudRuntimeException("Failed to delete export policy: " + e.getMessage(), e); + } + } catch (Exception e) { + s_logger.error("deleteAccessGroup: Failed to delete export policy. Exception: {}", e.getMessage(), e); + throw new CloudRuntimeException("Failed to delete export policy: " + e.getMessage(), e); + } } @Override @@ -404,13 +439,10 @@ private boolean updateFile(String volumeUuid, String filePath, FileInfo fileInfo } } - private String generateExportPolicyName(String svmName, String volumeName){ - return Constants.EXPORT + Constants.HYPHEN + svmName + Constants.HYPHEN + volumeName; - } private ExportPolicy createExportPolicyRequest(AccessGroup accessGroup,String svmName , String volumeName){ - String exportPolicyName = generateExportPolicyName(svmName,volumeName); + String exportPolicyName = Utility.generateExportPolicyName(svmName,volumeName); ExportPolicy exportPolicy = new ExportPolicy(); List rules = new ArrayList<>(); diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java index e73d726d97ea..5909a540af24 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/utils/Constants.java @@ -33,6 +33,8 @@ public class Constants { public static final String MANAGEMENT_LIF = "managementLIF"; public static final String VOLUME_NAME = "volumeName"; public static final String VOLUME_UUID = "volumeUUID"; + public static final String EXPORT_POLICY_ID = "exportPolicyId"; + public static final String EXPORT_POLICY_NAME = "exportPolicyName"; public static final String IS_DISAGGREGATED = "isDisaggregated"; public static final String RUNNING = "running"; public static final String EXPORT = "export"; From 31dd3cc435e88877a843ef75abd3254efcd243fd Mon Sep 17 00:00:00 2001 From: "Jain, Rajiv" Date: Wed, 21 Jan 2026 10:38:21 +0530 Subject: [PATCH 4/4] CSTACKEX-18: resolve spell check --- .../cloudstack/storage/service/model/CloudStackVolume.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 0153f7f6031a..c62d093fe071 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 @@ -27,12 +27,12 @@ public class CloudStackVolume { /** * Filed used for request: - * a. snapshot worflows will get source file details from it. + * a. snapshot workflows will get source file details from it. */ private FileInfo file; /** * Filed used for request: - * a. snapshot worflows will get source LUN details from it. + * a. snapshot workflows will get source LUN details from it. */ private Lun lun; private String datastoreId;