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 04996b74d2a5..fdc21fd7d00f 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 @@ -54,11 +54,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.client.SnapshotFeignClient; -import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot; import org.apache.cloudstack.storage.feign.model.Lun; -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.UnifiedSANStrategy; @@ -67,7 +63,6 @@ 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; @@ -211,7 +206,7 @@ private CloudStackVolume createCloudStackVolume(StoragePoolVO storagePool, Volum * Deletes a volume or snapshot from the ONTAP storage system. * *

For volumes, deletes the backend storage object (LUN for iSCSI, no-op for NFS). - * For snapshots, deletes the FlexVolume snapshot from ONTAP that was created by takeSnapshot.

+ * For snapshots, deletes the clone artifact from ONTAP that was created by takeSnapshot.

*/ @Override public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallback callback) { @@ -237,8 +232,8 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac 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); + // Delete the clone object (file/LUN) that was created by takeSnapshot + deleteCloneBackedSnapshot((SnapshotInfo) data, commandResult); } else { throw new CloudRuntimeException("Unsupported data object type: " + data.getType()); } @@ -252,30 +247,23 @@ public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbac } /** - * Deletes an ONTAP FlexVolume snapshot. - * - *

Retrieves the snapshot details stored during takeSnapshot and calls the ONTAP - * REST API to delete the FlexVolume snapshot.

- * - * @param snapshotInfo The CloudStack snapshot to delete - * @param commandResult Result object to populate with success/failure + * Deletes a clone-backed ONTAP snapshot object (NFS file clone or iSCSI LUN clone). */ - private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult commandResult) { + private void deleteCloneBackedSnapshot(SnapshotInfo snapshotInfo, CommandResult commandResult) { long snapshotId = snapshotInfo.getId(); - logger.info("deleteOntapSnapshot: Deleting ONTAP FlexVolume snapshot for CloudStack snapshot [{}]", snapshotId); + logger.info("deleteCloneBackedSnapshot: Deleting clone-backed ONTAP object for CloudStack snapshot [{}]", snapshotId); try { - // Retrieve snapshot details stored during takeSnapshot String flexVolUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.BASE_ONTAP_FV_ID); - String ontapSnapshotUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_ID); - String snapshotName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); + String cloneUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_ID); + String cloneName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_NAME); String poolIdStr = getSnapshotDetail(snapshotId, OntapStorageConstants.PRIMARY_POOL_ID); + String protocol = getSnapshotDetail(snapshotId, OntapStorageConstants.PROTOCOL); - if (flexVolUuid == null || ontapSnapshotUuid == null) { - logger.warn("deleteOntapSnapshot: Missing ONTAP snapshot details for snapshot [{}]. " + - "flexVolUuid={}, ontapSnapshotUuid={}. Snapshot may have been created by a different method or already deleted.", - snapshotId, flexVolUuid, ontapSnapshotUuid); - // Consider this a success since there's nothing to delete on ONTAP + if (poolIdStr == null || protocol == null || cloneName == null) { + logger.warn("deleteCloneBackedSnapshot: Missing clone metadata for snapshot [{}]. " + + "poolId={}, protocol={}, cloneName={}. Treating as success.", + snapshotId, poolIdStr, protocol, cloneName); commandResult.setSuccess(true); commandResult.setResult(null); return; @@ -285,26 +273,17 @@ private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult comman Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(poolId); StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - SnapshotFeignClient snapshotClient = storageStrategy.getSnapshotFeignClient(); - String authHeader = storageStrategy.getAuthHeader(); - - logger.info("deleteOntapSnapshot: Deleting ONTAP snapshot [{}] (uuid={}) from FlexVol [{}]", - snapshotName, ontapSnapshotUuid, flexVolUuid); - - // Call ONTAP REST API to delete the snapshot - JobResponse jobResponse = snapshotClient.deleteSnapshot(authHeader, flexVolUuid, ontapSnapshotUuid); - - if (jobResponse != null && jobResponse.getJob() != null) { - // Poll for job completion - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); - if (!jobSucceeded) { - throw new CloudRuntimeException("Delete job failed for snapshot [" + - snapshotName + "] on FlexVol [" + flexVolUuid + "]"); - } + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + logger.warn("deleteCloneBackedSnapshot: Missing FlexVol UUID for clone delete on snapshot [{}]. Treating as success.", snapshotId); + commandResult.setSuccess(true); + commandResult.setResult(null); + return; } + storageStrategy.deleteSnapshotClone(flexVolUuid, + poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName, cloneUuid); - logger.info("deleteOntapSnapshot: Successfully deleted ONTAP snapshot [{}] (uuid={}) for CloudStack snapshot [{}]", - snapshotName, ontapSnapshotUuid, snapshotId); + logger.info("deleteCloneBackedSnapshot: Successfully deleted clone object [{}] for CloudStack snapshot [{}]", + cloneName, snapshotId); commandResult.setSuccess(true); commandResult.setResult(null); @@ -314,12 +293,12 @@ private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult comman String errorMsg = e.getMessage(); if (errorMsg != null && (errorMsg.contains("404") || errorMsg.contains("not found") || errorMsg.contains("does not exist"))) { - logger.warn("deleteOntapSnapshot: ONTAP snapshot for CloudStack snapshot [{}] not found, " + + logger.warn("deleteCloneBackedSnapshot: Snapshot clone object for CloudStack snapshot [{}] not found, " + "may have been already deleted. Treating as success.", snapshotId); commandResult.setSuccess(true); commandResult.setResult(null); } else { - logger.error("deleteOntapSnapshot: Failed to delete ONTAP snapshot for CloudStack snapshot [{}]: {}", + logger.error("deleteCloneBackedSnapshot: Failed to delete clone-backed snapshot object for CloudStack snapshot [{}]: {}", snapshotId, e.getMessage(), e); commandResult.setSuccess(false); commandResult.setResult(e.getMessage()); @@ -621,16 +600,12 @@ public long getUsedIops(StoragePool storagePool) { } /** - * Takes a snapshot by creating an ONTAP FlexVolume-level snapshot. + * Takes a snapshot by creating an ONTAP clone artifact. * - *

This method creates a point-in-time, space-efficient snapshot of the entire - * FlexVolume containing the CloudStack volume. FlexVolume snapshots are atomic - * and capture all files/LUNs within the volume at the moment of creation.

- * - *

Both NFS and iSCSI protocols use the same FlexVolume snapshot approach: + *

Both NFS and iSCSI protocols use clone-backed snapshot semantics: *

*

* @@ -639,8 +614,14 @@ public long getUsedIops(StoragePool storagePool) { */ @Override public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback callback) { - logger.info("OntapPrimaryDatastoreDriver.takeSnapshot: Creating FlexVolume snapshot for snapshot [{}]", snapshot.getId()); + logger.info("OntapPrimaryDatastoreDriver.takeSnapshot: Creating clone-backed snapshot for snapshot [{}]", snapshot.getId()); CreateCmdResult result; + StorageStrategy storageStrategy = null; + String protocol = null; + String flexVolUuid = null; + String cloneName = null; + String cloneId = null; + Map poolDetails = null; try { VolumeInfo volumeInfo = snapshot.getBaseVolume(); @@ -656,83 +637,82 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId()); - String protocol = poolDetails.get(OntapStorageConstants.PROTOCOL); - String flexVolUuid = poolDetails.get(OntapStorageConstants.VOLUME_UUID); + poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId()); + protocol = poolDetails.get(OntapStorageConstants.PROTOCOL); + flexVolUuid = poolDetails.get(OntapStorageConstants.VOLUME_UUID); if (flexVolUuid == null || flexVolUuid.isEmpty()) { throw new CloudRuntimeException("FlexVolume UUID not found in pool details for pool " + volumeVO.getPoolId()); } - StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - SnapshotFeignClient snapshotClient = storageStrategy.getSnapshotFeignClient(); - String authHeader = storageStrategy.getAuthHeader(); + storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO) snapshot.getTO(); - - // Build snapshot name using volume name and snapshot UUID - String snapshotName = buildSnapshotName(volumeInfo.getName(), snapshot.getUuid()); - - // Resolve the volume path for storing in snapshot details (for revert operation) + String cloudStackSnapshotName = snapshot.getName(); + cloneName = OntapStorageUtils.getOntapCloneName(cloudStackSnapshotName); String volumePath = resolveVolumePathOnOntap(volumeVO, protocol, poolDetails); - - // For iSCSI, retrieve LUN UUID for restore operations String lunUuid = null; + String sourceObjectUuid = null; + if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { VolumeDetailVO lunDetail = volumeDetailsDao.findDetail(volumeVO.getId(), OntapStorageConstants.LUN_DOT_UUID); - String lunUUID = lunDetail != null ? lunDetail.getValue() : null; - if (lunUUID == null) { + lunUuid = lunDetail != null ? lunDetail.getValue() : null; + if (lunUuid == null) { throw new CloudRuntimeException("LUN UUID not found for iSCSI volume " + volumeVO.getId()); } + sourceObjectUuid = lunUuid; } - // Create FlexVolume snapshot via ONTAP REST API - FlexVolSnapshot snapshotRequest = new FlexVolSnapshot(snapshotName, - "CloudStack volume snapshot for volume " + volumeInfo.getName()); - - logger.info("takeSnapshot: Creating ONTAP FlexVolume snapshot [{}] on FlexVol UUID [{}] for volume [{}]", - snapshotName, flexVolUuid, volumeVO.getId()); + logger.info("takeSnapshot: Creating {} clone [{}] from source [{}] on FlexVol [{}]", + protocol, cloneName, volumePath, flexVolUuid); + cloneId = storageStrategy.createSnapshotClone( + flexVolUuid, poolDetails.get(OntapStorageConstants.VOLUME_NAME), volumePath, cloneName, sourceObjectUuid); - JobResponse jobResponse = snapshotClient.createSnapshot(authHeader, flexVolUuid, snapshotRequest); - if (jobResponse == null || jobResponse.getJob() == null) { - throw new CloudRuntimeException("Failed to initiate FlexVolume snapshot on FlexVol UUID [" + flexVolUuid + "]"); - } - - // Poll for job completion - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); - if (!jobSucceeded) { - throw new CloudRuntimeException("FlexVolume snapshot job failed on FlexVol UUID [" + flexVolUuid + "]"); - } - - // Retrieve the created snapshot UUID by name - String ontapSnapshotUuid = resolveSnapshotUuid(snapshotClient, authHeader, flexVolUuid, snapshotName); - if (ontapSnapshotUuid == null || ontapSnapshotUuid.isEmpty()) { - throw new CloudRuntimeException("Failed to resolve snapshot UUID for snapshot name [" + snapshotName + "]"); - } - - // Set snapshot path for CloudStack (format: snapshotName for identification) - snapshotObjectTo.setPath(OntapStorageConstants.ONTAP_SNAP_ID + "=" + ontapSnapshotUuid); + snapshotObjectTo.setPath(OntapStorageConstants.ONTAP_CLONE_NAME + "=" + cloneName); // Persist snapshot details for revert/delete operations updateSnapshotDetails(snapshot.getId(), volumeInfo.getId(), flexVolUuid, - ontapSnapshotUuid, snapshotName, volumePath, volumeVO.getPoolId(), protocol, lunUuid); + cloneId, cloudStackSnapshotName, cloneName, volumePath, volumeVO.getPoolId(), protocol, lunUuid); CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo); result = new CreateCmdResult(null, createObjectAnswer); result.setResult(null); - logger.info("takeSnapshot: Successfully created FlexVolume snapshot [{}] (uuid={}) for volume [{}]", - snapshotName, ontapSnapshotUuid, volumeVO.getId()); + logger.info("takeSnapshot: Successfully created clone-backed snapshot [{}] (clone={}) for volume [{}]", + cloudStackSnapshotName, cloneName, volumeVO.getId()); } catch (Exception ex) { - logger.error("takeSnapshot: Failed due to ", ex); - result = new CreateCmdResult(null, new CreateObjectAnswer(ex.toString())); - result.setResult(ex.toString()); + String rollbackStatus = rollbackPartialSnapshotClone(storageStrategy, flexVolUuid, + poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName, cloneId); + String errorWithRollback = ex.toString() + " | rollbackStatus=" + rollbackStatus; + logger.error("takeSnapshot: Failed with rollback status [{}]", rollbackStatus, ex); + result = new CreateCmdResult(null, new CreateObjectAnswer(errorWithRollback)); + result.setResult(errorWithRollback); } callback.complete(result); } + /** + * Best-effort rollback of partially created snapshot clone objects when takeSnapshot fails. + * Returns a status string that is appended to the task result so CloudStack has clear context. + */ + private String rollbackPartialSnapshotClone(StorageStrategy storageStrategy, String flexVolUuid, + String flexVolName, String cloneName, String cloneUuid) { + if (storageStrategy == null || cloneName == null || cloneName.isEmpty()) { + return "not-attempted"; + } + try { + storageStrategy.deleteSnapshotClone(flexVolUuid, flexVolName, cloneName, cloneUuid); + return "clone-deleted"; + } catch (Exception cleanupEx) { + String cleanupMessage = cleanupEx.getMessage() != null ? cleanupEx.getMessage() : cleanupEx.toString(); + logger.warn("rollbackPartialSnapshotClone: Failed to clean up clone [{}]: {}", + cloneName, cleanupMessage); + return "cleanup-failed:" + cleanupMessage; + } + } + /** * Resolves the volume path on ONTAP for snapshot restore operations. * @@ -761,42 +741,16 @@ private String resolveVolumePathOnOntap(VolumeVO volumeVO, String protocol, Map< } /** - * Resolves the ONTAP snapshot UUID by querying for the snapshot by name. - * - * @param snapshotClient The ONTAP snapshot Feign client - * @param authHeader Authorization header - * @param flexVolUuid FlexVolume UUID - * @param snapshotName Name of the snapshot to find - * @return The UUID of the snapshot, or null if not found - */ - private String resolveSnapshotUuid(SnapshotFeignClient snapshotClient, String authHeader, - String flexVolUuid, String snapshotName) { - Map queryParams = new HashMap<>(); - queryParams.put("name", snapshotName); - queryParams.put("fields", "uuid,name"); - - OntapResponse response = snapshotClient.getSnapshots(authHeader, flexVolUuid, queryParams); - if (response != null && response.getRecords() != null && !response.getRecords().isEmpty()) { - return response.getRecords().get(0).getUuid(); - } - return null; - } - - /** - * Reverts a volume to a snapshot using protocol-specific ONTAP restore APIs. + * Reverts a volume using protocol-specific ONTAP clone restore APIs. * *

This method delegates to the appropriate StorageStrategy to restore the - * specific file (NFS) or LUN (iSCSI) from the FlexVolume snapshot directly - * via ONTAP REST API, without involving the hypervisor agent.

+ * specific file (NFS) or LUN (iSCSI) from clone artifacts directly via ONTAP + * REST API, without involving the hypervisor agent.

* *

Protocol-specific handling (delegated to strategy classes):

*
    - *
  • NFS (UnifiedNASStrategy): Uses the single-file restore API: - * {@code POST /api/storage/volumes/{volume_uuid}/snapshots/{snapshot_uuid}/files/{file_path}/restore} - * Restores the QCOW2 file from the FlexVolume snapshot to its original location.
  • - *
  • iSCSI (UnifiedSANStrategy): Uses the LUN restore API: - * {@code POST /api/storage/luns/{lun.uuid}/restore} - * Restores the LUN data from the snapshot to the specified destination path.
  • + *
  • NFS (UnifiedNASStrategy): Creates/restores data from file clone artifacts.
  • + *
  • iSCSI (UnifiedSANStrategy): Restores data from LUN clone artifacts via LUN patch clone source.
  • *
*/ @Override @@ -814,15 +768,19 @@ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snaps // Retrieve snapshot details stored during takeSnapshot String flexVolUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.BASE_ONTAP_FV_ID); - String ontapSnapshotUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_ID); - String snapshotName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); + String ontapCloneId = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_ID); + String ontapCloneName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_NAME); + if (ontapCloneName == null) { + // Backward compatibility for snapshots created before clone-name metadata was persisted. + ontapCloneName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); + } String volumePath = getSnapshotDetail(snapshotId, OntapStorageConstants.VOLUME_PATH); String poolIdStr = getSnapshotDetail(snapshotId, OntapStorageConstants.PRIMARY_POOL_ID); String protocol = getSnapshotDetail(snapshotId, OntapStorageConstants.PROTOCOL); - if (flexVolUuid == null || snapshotName == null || volumePath == null || poolIdStr == null) { + if (flexVolUuid == null || ontapCloneName == null || volumePath == null || poolIdStr == null) { throw new CloudRuntimeException("Missing required snapshot details for snapshot " + snapshotId + - " (flexVolUuid=" + flexVolUuid + ", snapshotName=" + snapshotName + + " (flexVolUuid=" + flexVolUuid + ", cloneName=" + ontapCloneName + ", volumePath=" + volumePath + ", poolId=" + poolIdStr + ")"); } @@ -831,37 +789,22 @@ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snaps StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - // Get the FlexVolume name (required for CLI-based restore API for all protocols) + // Get the FlexVolume name required by strategy clone-restore operations. String flexVolName = poolDetails.get(OntapStorageConstants.VOLUME_NAME); if (flexVolName == null || flexVolName.isEmpty()) { throw new CloudRuntimeException("FlexVolume name not found in pool details for pool " + poolId); } - // Prepare protocol-specific parameters (lunUuid is only needed for backward compatibility) - String lunUuid = null; - if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { - lunUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.LUN_DOT_UUID); - } - - // Delegate to strategy class for protocol-specific restore - JobResponse jobResponse = storageStrategy.revertSnapshotForCloudStackVolume( - snapshotName, flexVolUuid, ontapSnapshotUuid, volumePath, lunUuid, flexVolName); + // Delegate protocol-specific restore behavior to SAN/NAS strategy implementations + storageStrategy.revertSnapshotForCloudStackVolume( + ontapCloneName, flexVolUuid, ontapCloneId, volumePath, flexVolName); - if (jobResponse == null || jobResponse.getJob() == null) { - throw new CloudRuntimeException("Failed to initiate restore from snapshot [" + - snapshotName + "]"); - } - - // Poll for job completion (use longer timeout for large LUNs/files) - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2000); - if (!jobSucceeded) { - throw new CloudRuntimeException("Restore job failed for snapshot [" + - snapshotName + "]"); - } + logger.info("revertSnapshot: {} restore for [{}] completed using synchronous PATCH semantics", + ProtocolType.ISCSI.name().equalsIgnoreCase(protocol) ? "iSCSI" : "NFS", volumePath); - logger.info("revertSnapshot: Successfully restored {} [{}] from snapshot [{}]", + logger.info("revertSnapshot: Successfully restored {} [{}] from clone [{}]", ProtocolType.ISCSI.name().equalsIgnoreCase(protocol) ? "LUN" : "file", - volumePath, snapshotName); + volumePath, ontapCloneName); result.setResult(null); // Success @@ -974,38 +917,23 @@ private CloudStackVolume createDeleteCloudStackVolumeRequest(StoragePool storage // Snapshot Helper Methods // ────────────────────────────────────────────────────────────────────────── - /** - * Builds a snapshot name with proper length constraints. - * Format: {@code -} - */ - private String buildSnapshotName(String volumeName, String snapshotUuid) { - String name = volumeName + "-" + snapshotUuid; - int maxLength = OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH; - int trimRequired = name.length() - maxLength; - - if (trimRequired > 0) { - name = StringUtils.left(volumeName, volumeName.length() - trimRequired) + "-" + snapshotUuid; - } - return name; - } - /** * Persists snapshot metadata in snapshot_details table. * * @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 ontapCloneId ONTAP clone artifact UUID + * @param snapshotName CloudStack snapshot display name + * @param volumePath Path of the source volume file/LUN on ONTAP * @param storagePoolId Primary storage pool ID * @param protocol Storage protocol (NFS3 or ISCSI) * @param lunUuid LUN UUID (only for iSCSI, null for NFS) */ private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String flexVolUuid, - String ontapSnapshotUuid, String snapshotName, - String volumePath, long storagePoolId, String protocol, - String lunUuid) { + String ontapCloneId, String snapshotName, String ontapCloneName, + 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); @@ -1015,13 +943,21 @@ private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String fl snapshotDetailsDao.persist(snapshotDetail); snapshotDetail = new SnapshotDetailsVO(csSnapshotId, - OntapStorageConstants.ONTAP_SNAP_ID, ontapSnapshotUuid, false); + OntapStorageConstants.ONTAP_SNAP_ID, ontapCloneId, false); snapshotDetailsDao.persist(snapshotDetail); snapshotDetail = new SnapshotDetailsVO(csSnapshotId, OntapStorageConstants.ONTAP_SNAP_NAME, snapshotName, false); snapshotDetailsDao.persist(snapshotDetail); + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, + OntapStorageConstants.ONTAP_CLONE_ID, ontapCloneId, false); + snapshotDetailsDao.persist(snapshotDetail); + + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, + OntapStorageConstants.ONTAP_CLONE_NAME, ontapCloneName, false); + snapshotDetailsDao.persist(snapshotDetail); + snapshotDetail = new SnapshotDetailsVO(csSnapshotId, OntapStorageConstants.VOLUME_PATH, volumePath, false); snapshotDetailsDao.persist(snapshotDetail); @@ -1034,7 +970,7 @@ private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String fl OntapStorageConstants.PROTOCOL, protocol, false); snapshotDetailsDao.persist(snapshotDetail); - // Store LUN UUID for iSCSI volumes (required for LUN restore API) + // Store source/live LUN UUID for iSCSI volumes (used during clone-based restore). if (lunUuid != null && !lunUuid.isEmpty()) { snapshotDetail = new SnapshotDetailsVO(csSnapshotId, OntapStorageConstants.LUN_DOT_UUID, lunUuid, false); 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..2f30dfd92514 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; @@ -44,11 +46,12 @@ void deleteFile(@Param("authHeader") String authHeader, @Param("volumeUuid") String volumeUUID, @Param("path") String filePath); - @RequestLine("PATCH /api/storage/volumes/{volumeUuid}/files/{path}") + @RequestLine("PATCH /api/storage/volumes/{volumeUuid}/files/{path}?return_records={returnRecords}") @Headers({"Authorization: {authHeader}"}) void updateFile(@Param("authHeader") String authHeader, @Param("volumeUuid") String volumeUUID, @Param("path") String filePath, + @Param("returnRecords") boolean returnRecords, FileInfo fileInfo); @RequestLine("POST /api/storage/volumes/{volumeUuid}/files/{path}") @@ -58,6 +61,10 @@ void createFile(@Param("authHeader") String authHeader, @Param("path") String filePath, FileInfo file); + @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..b97367cc2df6 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 @@ -23,7 +23,6 @@ import org.apache.cloudstack.storage.feign.model.IscsiService; import org.apache.cloudstack.storage.feign.model.Lun; import org.apache.cloudstack.storage.feign.model.LunMap; -import org.apache.cloudstack.storage.feign.model.LunRestoreRequest; import org.apache.cloudstack.storage.feign.model.response.JobResponse; import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import feign.Headers; @@ -42,6 +41,10 @@ public interface SANFeignClient { @Headers({"Authorization: {authHeader}"}) OntapResponse createLun(@Param("authHeader") String authHeader, @Param("returnRecords") boolean returnRecords, Lun lun); + @RequestLine("POST /api/storage/luns") + @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) + JobResponse cloneLun(@Param("authHeader") String authHeader, Lun lun); + @RequestLine("GET /api/storage/luns") @Headers({"Authorization: {authHeader}"}) OntapResponse getLunResponse(@Param("authHeader") String authHeader, @QueryMap Map queryMap); @@ -50,7 +53,7 @@ public interface SANFeignClient { @Headers({"Authorization: {authHeader}"}) Lun getLunByUUID(@Param("authHeader") String authHeader, @Param("uuid") String uuid); - @RequestLine("PATCH /{uuid}") + @RequestLine("PATCH /api/storage/luns/{uuid}") @Headers({"Authorization: {authHeader}"}) void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun); @@ -90,24 +93,4 @@ public interface SANFeignClient { void deleteLunMap(@Param("authHeader") String authHeader, @Param("lunUuid") String lunUUID, @Param("igroupUuid") String igroupUUID); - - // LUN Restore API - /** - * Restores a LUN from a FlexVolume snapshot. - * - *

ONTAP REST: {@code POST /api/storage/luns/{lun.uuid}/restore}

- * - *

This API restores the LUN data from a specified snapshot to a destination path. - * The LUN must exist and the snapshot must contain the LUN data.

- * - * @param authHeader Basic auth header - * @param lunUuid UUID of the LUN to restore - * @param request Request body with snapshot name and destination path - * @return JobResponse containing the async job reference - */ - @RequestLine("POST /api/storage/luns/{lunUuid}/restore") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreLun(@Param("authHeader") String authHeader, - @Param("lunUuid") String lunUuid, - LunRestoreRequest request); } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SnapshotFeignClient.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SnapshotFeignClient.java deleted file mode 100644 index 2f0e050d6f55..000000000000 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SnapshotFeignClient.java +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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 feign.Headers; -import feign.Param; -import feign.QueryMap; -import feign.RequestLine; -import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest; -import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot; -import org.apache.cloudstack.storage.feign.model.SnapshotFileRestoreRequest; -import org.apache.cloudstack.storage.feign.model.response.JobResponse; -import org.apache.cloudstack.storage.feign.model.response.OntapResponse; - -import java.util.Map; - -/** - * Feign client for ONTAP FlexVolume snapshot operations. - * - *

Maps to the ONTAP REST API endpoint: - * {@code /api/storage/volumes/{volume_uuid}/snapshots}

- * - *

FlexVolume snapshots are point-in-time, space-efficient copies of an entire - * FlexVolume. Unlike file-level clones, a single FlexVolume snapshot atomically - * captures all files/LUNs within the volume, making it ideal for VM-level - * snapshots when multiple CloudStack disks reside on the same FlexVolume.

- */ -public interface SnapshotFeignClient { - - /** - * Creates a new snapshot for the specified FlexVolume. - * - *

ONTAP REST: {@code POST /api/storage/volumes/{volume_uuid}/snapshots}

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshot Snapshot request body (at minimum, the {@code name} field) - * @return JobResponse containing the async job reference - */ - @RequestLine("POST /api/storage/volumes/{volumeUuid}/snapshots") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse createSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - FlexVolSnapshot snapshot); - - /** - * Lists snapshots for the specified FlexVolume. - * - *

ONTAP REST: {@code GET /api/storage/volumes/{volume_uuid}/snapshots}

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param queryParams Optional query parameters (e.g., {@code name}, {@code fields}) - * @return Paginated response of FlexVolSnapshot records - */ - @RequestLine("GET /api/storage/volumes/{volumeUuid}/snapshots") - @Headers({"Authorization: {authHeader}"}) - OntapResponse getSnapshots(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @QueryMap Map queryParams); - - /** - * Retrieves a specific snapshot by UUID. - * - *

ONTAP REST: {@code GET /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot - * @return The FlexVolSnapshot object - */ - @RequestLine("GET /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}") - @Headers({"Authorization: {authHeader}"}) - FlexVolSnapshot getSnapshotByUuid(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Deletes a specific snapshot. - * - *

ONTAP REST: {@code DELETE /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot to delete - * @return JobResponse containing the async job reference - */ - @RequestLine("DELETE /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}") - @Headers({"Authorization: {authHeader}"}) - JobResponse deleteSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Restores a volume to a specific snapshot. - * - *

ONTAP REST: {@code PATCH /api/storage/volumes/{volume_uuid}/snapshots/{uuid}} - * with body {@code {"restore": true}} triggers a snapshot restore operation.

- * - *

Note: This is a destructive operation — all data written after the - * snapshot was taken will be lost.

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot to restore to - * @param body Request body, typically {@code {"restore": true}} - * @return JobResponse containing the async job reference - */ - @RequestLine("PATCH /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}?restore_to_snapshot=true") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Restores a single file or LUN from a FlexVolume snapshot. - * - *

ONTAP REST: - * {@code POST /api/storage/volumes/{volume_uuid}/snapshots/{snapshot_uuid}/files/{file_path}/restore}

- * - *

This restores only the specified file/LUN from the snapshot to the - * given {@code destination_path}, without reverting the entire FlexVolume. - * Ideal when multiple VMs share the same FlexVolume.

- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot containing the file - * @param filePath path of the file within the snapshot (URL-encoded if needed) - * @param request request body with {@code destination_path} - * @return JobResponse containing the async job reference - */ - @RequestLine("POST /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}/files/{filePath}/restore") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreFileFromSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid, - @Param("filePath") String filePath, - SnapshotFileRestoreRequest request); - - /** - * Restores a single file or LUN from a FlexVolume snapshot using the CLI native API. - * - *

ONTAP REST (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}

- * - *

This CLI-based API is more reliable and works for both NFS files and iSCSI LUNs. - * The request body contains all required parameters: vserver, volume, snapshot, and path.

- * - *

Example payload: - *

-     * {
-     *   "vserver": "vs0",
-     *   "volume": "rajiv_ONTAP_SP1",
-     *   "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
-     *   "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
-     * }
-     * 
- *

- * - * @param authHeader Basic auth header - * @param request CLI snapshot restore request containing vserver, volume, snapshot, and path - * @return JobResponse containing the async job reference (if applicable) - */ - @RequestLine("POST /api/private/cli/volume/snapshot/restore-file") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreFileFromSnapshotCli(@Param("authHeader") String authHeader, - CliSnapshotRestoreRequest request); -} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java deleted file mode 100644 index be242523f534..000000000000 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * 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 CLI-based Snapshot File Restore API. - * - *

ONTAP REST endpoint (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}

- * - *

This API restores a single file or LUN from a FlexVolume snapshot to a - * specified destination path using the CLI native implementation. - * It works for both NFS files and iSCSI LUNs.

- * - *

Example payload: - *

- * {
- *   "vserver": "vs0",
- *   "volume": "rajiv_ONTAP_SP1",
- *   "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
- *   "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
- * }
- * 
- *

- */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -public class CliSnapshotRestoreRequest { - - @JsonProperty("vserver") - private String vserver; - - @JsonProperty("volume") - private String volume; - - @JsonProperty("snapshot") - private String snapshot; - - @JsonProperty("path") - private String path; - - public CliSnapshotRestoreRequest() { - } - - /** - * Creates a CLI snapshot restore request. - * - * @param vserver The SVM (vserver) name - * @param volume The FlexVolume name - * @param snapshot The snapshot name - * @param path The file/LUN path to restore (e.g., "/uuid.qcow2" or "/lun_name") - */ - public CliSnapshotRestoreRequest(String vserver, String volume, String snapshot, String path) { - this.vserver = vserver; - this.volume = volume; - this.snapshot = snapshot; - this.path = path; - } - - public String getVserver() { - return vserver; - } - - public void setVserver(String vserver) { - this.vserver = vserver; - } - - public String getVolume() { - return volume; - } - - public void setVolume(String volume) { - this.volume = volume; - } - - public String getSnapshot() { - return snapshot; - } - - public void setSnapshot(String snapshot) { - this.snapshot = snapshot; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - @Override - public String toString() { - return "CliSnapshotRestoreRequest{" + - "vserver='" + vserver + '\'' + - ", volume='" + volume + '\'' + - ", snapshot='" + snapshot + '\'' + - ", path='" + path + '\'' + - '}'; - } -} diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java similarity index 57% rename from plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java rename to plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java index 1f02e0c07470..d3df9bc64ce2 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java @@ -18,31 +18,34 @@ */ 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 Snapshot File Restore API. - * - *

ONTAP REST endpoint: - * {@code POST /api/storage/volumes/{volume.uuid}/snapshots/{snapshot.uuid}/files/{file.path}/restore}

- * - *

This API restores a single file or LUN from a FlexVolume snapshot to a - * specified destination path, without reverting the entire FlexVolume.

- */ -@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -public class SnapshotFileRestoreRequest { +public class FileCloneRequest { + @JsonProperty("volume") + private VolumeRef volume; + + @JsonProperty("source_path") + private String sourcePath; @JsonProperty("destination_path") private String destinationPath; - public SnapshotFileRestoreRequest() { + public VolumeRef getVolume() { + return volume; } - public SnapshotFileRestoreRequest(String destinationPath) { - this.destinationPath = destinationPath; + public void setVolume(VolumeRef volume) { + this.volume = volume; + } + + public String getSourcePath() { + return sourcePath; + } + + public void setSourcePath(String sourcePath) { + this.sourcePath = sourcePath; } public String getDestinationPath() { @@ -52,4 +55,29 @@ public String getDestinationPath() { public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class VolumeRef { + @JsonProperty("name") + private String name; + + @JsonProperty("uuid") + private String uuid; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + } } 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..23c8d30f2f60 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 @@ -86,6 +86,12 @@ public static PropertyClassEnum fromValue(String value) { @JsonProperty("clone") private Clone clone = null; + @JsonProperty("location") + private Location location = null; + + @JsonProperty("is_override") + private Boolean isOverride = null; + /** * The operating system type of the LUN.<br/> Required in POST when creating a LUN that is not a clone of another. Disallowed in POST when creating a LUN clone. */ @@ -260,6 +266,22 @@ public void setClone(Clone clone) { this.clone = clone; } + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public Boolean getIsOverride() { + return isOverride; + } + + public void setIsOverride(Boolean isOverride) { + this.isOverride = isOverride; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -287,11 +309,14 @@ public String toString() { sb.append(" enabled: ").append(toIndentedString(enabled)).append("\n"); sb.append(" lunMaps: ").append(toIndentedString(lunMaps)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" clone: ").append(toIndentedString(clone)).append("\n"); sb.append(" osType: ").append(toIndentedString(osType)).append("\n"); sb.append(" serialNumber: ").append(toIndentedString(serialNumber)).append("\n"); sb.append(" space: ").append(toIndentedString(space)).append("\n"); sb.append(" svm: ").append(toIndentedString(svm)).append("\n"); sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" isOverride: ").append(toIndentedString(isOverride)).append("\n"); sb.append("}"); return sb.toString(); } @@ -317,6 +342,15 @@ public Source getSource() { public void setSource(Source source) { this.source = source; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Clone {\n"); + sb.append(" source: ").append(source).append("\n"); + sb.append("}"); + return sb.toString(); + } } public static class Source { @@ -337,5 +371,59 @@ public String getUuid() { public void setUuid(String uuid) { this.uuid = uuid; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Source {\n"); + sb.append(" name: ").append(name).append("\n"); + sb.append(" uuid: ").append(uuid).append("\n"); + sb.append("}"); + return sb.toString(); + } + } + + public static class Location { + @JsonProperty("volume") + private LocationVolume volume = null; + + public LocationVolume getVolume() { + return volume; + } + + public void setVolume(LocationVolume volume) { + this.volume = volume; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Location {\n"); + sb.append(" volume: ").append(volume).append("\n"); + sb.append("}"); + return sb.toString(); + } + } + + public static class LocationVolume { + @JsonProperty("name") + private String name = null; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class LocationVolume {\n"); + sb.append(" name: ").append(name).append("\n"); + sb.append("}"); + return sb.toString(); + } } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java deleted file mode 100644 index c645e4a5a16f..000000000000 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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 LUN Restore API. - * - *

ONTAP REST endpoint: - * {@code POST /api/storage/luns/{lun.uuid}/restore}

- * - *

This API restores a LUN from a FlexVolume snapshot to a specified - * destination path. Unlike file restore, this is LUN-specific.

- * - *

Example payload: - *

- * {
- *   "snapshot": {
- *     "name": "snapshot_name"
- *   },
- *   "destination": {
- *     "path": "/vol/volume_name/lun_name"
- *   }
- * }
- * 
- *

- */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(JsonInclude.Include.NON_NULL) -public class LunRestoreRequest { - - @JsonProperty("snapshot") - private SnapshotRef snapshot; - - @JsonProperty("destination") - private Destination destination; - - public LunRestoreRequest() { - } - - public LunRestoreRequest(String snapshotName, String destinationPath) { - this.snapshot = new SnapshotRef(snapshotName); - this.destination = new Destination(destinationPath); - } - - public SnapshotRef getSnapshot() { - return snapshot; - } - - public void setSnapshot(SnapshotRef snapshot) { - this.snapshot = snapshot; - } - - public Destination getDestination() { - return destination; - } - - public void setDestination(Destination destination) { - this.destination = destination; - } - - /** - * Nested class for snapshot reference. - */ - @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; - } - } - - /** - * Nested class for destination path. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static class Destination { - - @JsonProperty("path") - private String path; - - public Destination() { - } - - public Destination(String path) { - this.path = path; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - } -} 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 bd808a26d6f8..8abdaac55bb2 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 @@ -27,7 +27,6 @@ import org.apache.cloudstack.storage.feign.client.NetworkFeignClient; import org.apache.cloudstack.storage.feign.client.NASFeignClient; import org.apache.cloudstack.storage.feign.client.SANFeignClient; -import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient; import org.apache.cloudstack.storage.feign.client.SvmFeignClient; import org.apache.cloudstack.storage.feign.client.VolumeFeignClient; import org.apache.cloudstack.storage.feign.model.Aggregate; @@ -70,7 +69,6 @@ public abstract class StorageStrategy { protected NetworkFeignClient networkFeignClient; protected SANFeignClient sanFeignClient; protected NASFeignClient nasFeignClient; - protected SnapshotFeignClient snapshotFeignClient; protected OntapStorage storage; @@ -94,7 +92,6 @@ public StorageStrategy(OntapStorage ontapStorage) { this.networkFeignClient = feignClientFactory.createClient(NetworkFeignClient.class, baseURL); this.sanFeignClient = feignClientFactory.createClient(SANFeignClient.class, baseURL); this.nasFeignClient = feignClientFactory.createClient(NASFeignClient.class, baseURL); - this.snapshotFeignClient = feignClientFactory.createClient(SnapshotFeignClient.class, baseURL); } // Connect method to validate ONTAP cluster, credentials, protocol, and SVM @@ -527,27 +524,51 @@ public String getNetworkInterface() { abstract public CloudStackVolume getCloudStackVolume(Map cloudStackVolumeMap); /** - * Reverts a CloudStack volume to a snapshot using protocol-specific ONTAP APIs. + * Reverts a CloudStack volume from a clone artifact using protocol-specific ONTAP APIs. * - *

This method encapsulates the snapshot revert behavior based on protocol:

+ *

This method encapsulates clone-based restore behavior based on protocol:

*
    - *
  • iSCSI/FC: Uses {@code POST /api/storage/luns/{lun.uuid}/restore} - * to restore LUN data from the FlexVolume snapshot.
  • - *
  • NFS: Uses {@code POST /api/storage/volumes/{vol.uuid}/snapshots/{snap.uuid}/files/{path}/restore} - * to restore a single file from the FlexVolume snapshot.
  • + *
  • iSCSI/FC: restores destination LUN data from a source clone LUN.
  • + *
  • NFS: restores destination file data from a source clone file.
  • *
* - * @param snapshotName The ONTAP FlexVolume snapshot name - * @param flexVolUuid The FlexVolume UUID containing the snapshot - * @param snapshotUuid The ONTAP snapshot UUID (used for NFS file restore) - * @param volumePath The path of the file/LUN within the FlexVolume - * @param lunUuid The LUN UUID (only for iSCSI, null for NFS) + * @param snapshotName The ONTAP source clone name/path token + * @param flexVolUuid The FlexVolume UUID containing the source clone + * @param snapshotUuid The ONTAP clone artifact UUID (used by SAN restore, ignored by NAS) + * @param volumePath The destination file/LUN path in the FlexVolume * @param flexVolName The FlexVolume name (only for iSCSI, for constructing destination path) - * @return JobResponse for the async restore operation + * @return void */ - public abstract JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, + public abstract void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, - String lunUuid, String flexVolName); + String flexVolName); + + /** + * Creates a protocol-specific clone artifact used to represent a snapshot point. + * + *

Implementations own all protocol-specific ONTAP interactions including any + * required async job polling.

+ * + * @param flexVolUuid FlexVolume UUID that contains the source object + * @param flexVolName FlexVolume name for path construction when needed + * @param sourcePath source file/LUN path + * @param cloneName destination clone name/path token + * @param sourceObjectUuid source object UUID (required by SAN, ignored by NAS) + * @return clone artifact UUID (or stable clone identifier for NAS) + */ + public abstract String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid); + + /** + * Deletes a protocol-specific clone artifact created for snapshot workflows. + * + * @param flexVolUuid FlexVolume UUID containing the clone + * @param flexVolName FlexVolume name for path construction when needed + * @param cloneName clone name/path token + * @param cloneObjectUuid clone UUID (required by SAN, ignored by NAS) + */ + public abstract void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, + String cloneObjectUuid); /** @@ -613,15 +634,6 @@ public abstract JobResponse revertSnapshotForCloudStackVolume(String snapshotNam */ abstract public String getLogicalAccess(Map values); - // ── FlexVolume Snapshot accessors ──────────────────────────────────────── - - /** - * Returns the {@link SnapshotFeignClient} for ONTAP FlexVolume snapshot operations. - */ - public SnapshotFeignClient getSnapshotFeignClient() { - return snapshotFeignClient; - } - /** * Returns the {@link NASFeignClient} for ONTAP NAS file operations * (including file clone for single-file SnapRestore). @@ -630,6 +642,10 @@ public NASFeignClient getNasFeignClient() { return nasFeignClient; } + public SANFeignClient getSanFeignClient() { + return sanFeignClient; + } + /** * Generates the Basic-auth header for ONTAP REST calls. */ 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 477e92630387..e9cdfe50ac10 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 @@ -34,6 +34,7 @@ import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; 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; @@ -42,7 +43,6 @@ 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.feign.model.CliSnapshotRestoreRequest; import org.apache.cloudstack.storage.service.model.AccessGroup; import org.apache.cloudstack.storage.service.model.CloudStackVolume; import org.apache.cloudstack.storage.volume.VolumeObject; @@ -434,52 +434,159 @@ private FileInfo getFile(String volumeUuid, String filePath) { } /** - * Reverts a file to a snapshot using the ONTAP CLI-based snapshot file restore API. + * Reverts an NFS file from clone artifact data. * - *

ONTAP REST API (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}

- * - *

This method uses the CLI native API which is more reliable and works - * consistently for both NFS files and iSCSI LUNs.

- * - * @param snapshotName The ONTAP FlexVolume snapshot name - * @param flexVolUuid The FlexVolume UUID (not used in CLI API, kept for interface consistency) - * @param snapshotUuid The ONTAP snapshot UUID (not used in CLI API, kept for interface consistency) - * @param volumePath The file path within the FlexVolume - * @param lunUuid Not used for NFS (null) - * @param flexVolName The FlexVolume name (required for CLI API) - * @return JobResponse for the async restore operation + *

The source clone file is copied back into the destination live file path. + * If destination already exists, this method uses a temporary backup path to + * preserve rollback safety.

*/ @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, - String lunUuid, String flexVolName) { - logger.info("revertSnapshotForCloudStackVolume [NFS]: Restoring file [{}] from snapshot [{}] on FlexVol [{}]", + String flexVolName) { + logger.info("revertSnapshotForCloudStackVolume [NFS]: Reverting file [{}] using clone [{}] on FlexVol [{}]", volumePath, snapshotName, flexVolName); if (snapshotName == null || snapshotName.isEmpty()) { - throw new CloudRuntimeException("Snapshot name is required for NFS snapshot revert"); + throw new CloudRuntimeException("Clone name is required for NFS snapshot revert"); } if (volumePath == null || volumePath.isEmpty()) { throw new CloudRuntimeException("File path is required for NFS snapshot revert"); } - if (flexVolName == null || flexVolName.isEmpty()) { - throw new CloudRuntimeException("FlexVolume name is required for NFS snapshot revert"); + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot revert"); } String authHeader = getAuthHeader(); - String svmName = storage.getSvmName(); + FileCloneRequest cloneRequest = new FileCloneRequest(); + FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(flexVolName); + cloneRequest.setVolume(volumeRef); + cloneRequest.setSourcePath(snapshotName); + cloneRequest.setDestinationPath(volumePath); + + try { + logger.debug("revertSnapshotForCloudStackVolume [NFS]: clone file source={} destination={}", + snapshotName, volumePath); + JobResponse cloneJobResponse = getNasFeignClient().cloneFile(authHeader, cloneRequest); + if (cloneJobResponse == null || cloneJobResponse.getJob() == null || + cloneJobResponse.getJob().getUuid() == null || cloneJobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException(String.format( + "cloneFile did not return a valid job response for source [%s] and destination [%s]", + snapshotName, volumePath)); + } + String cloneJobUuid = cloneJobResponse.getJob().getUuid(); + Boolean jobSucceeded = jobPollForSuccess(cloneJobUuid, 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException(String.format( + "cloneFile job [%s] failed for source [%s] and destination [%s]", + cloneJobUuid, snapshotName, volumePath)); + } + } catch (FeignException cloneEx) { + if (!isFileAlreadyExistsConflict(cloneEx)) { + throw cloneEx; + } + + String backupPath = volumePath + ".pre_revert_" + System.currentTimeMillis(); + logger.info("revertSnapshotForCloudStackVolume [NFS]: Destination [{}] exists, using backup path [{}] for safe restore", + volumePath, backupPath); + + FileInfo renameToBackup = new FileInfo(); + renameToBackup.setPath(backupPath); + getNasFeignClient().updateFile(authHeader, flexVolUuid, volumePath, true, renameToBackup); + + try { + JobResponse cloneJobResponse = getNasFeignClient().cloneFile(authHeader, cloneRequest); + if (cloneJobResponse == null || cloneJobResponse.getJob() == null || + cloneJobResponse.getJob().getUuid() == null || cloneJobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException(String.format( + "cloneFile did not return a valid job response for source [%s] and destination [%s]", + snapshotName, volumePath)); + } + String cloneJobUuid = cloneJobResponse.getJob().getUuid(); + Boolean jobSucceeded = jobPollForSuccess(cloneJobUuid, 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException(String.format( + "cloneFile job [%s] failed for source [%s] and destination [%s]", + cloneJobUuid, snapshotName, volumePath)); + } + try { + getNasFeignClient().deleteFile(authHeader, flexVolUuid, backupPath); + } catch (Exception cleanupEx) { + logger.warn("revertSnapshotForCloudStackVolume [NFS]: Backup cleanup failed for [{}]: {}", + backupPath, cleanupEx.getMessage()); + } + } catch (Exception restoreEx) { + try { + FileInfo rollbackRename = new FileInfo(); + rollbackRename.setPath(volumePath); + getNasFeignClient().updateFile(authHeader, flexVolUuid, backupPath, true, rollbackRename); + } catch (Exception rollbackEx) { + logger.error("revertSnapshotForCloudStackVolume [NFS]: Failed to roll back backup rename [{}] -> [{}]: {}", + backupPath, volumePath, rollbackEx.getMessage(), rollbackEx); + } + throw restoreEx; + } + } + + } - // Prepare the file path for ONTAP CLI API (ensure it starts with "/") - String ontapFilePath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + @Override + public String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid) { + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot clone create"); + } + if (sourcePath == null || sourcePath.isEmpty()) { + throw new CloudRuntimeException("Source file path is required for NFS snapshot clone create"); + } + if (cloneName == null || cloneName.isEmpty()) { + throw new CloudRuntimeException("Clone file name is required for NFS snapshot clone create"); + } - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapFilePath); + FileCloneRequest fileCloneRequest = new FileCloneRequest(); + FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(flexVolName); + fileCloneRequest.setVolume(volumeRef); + fileCloneRequest.setSourcePath(sourcePath); + fileCloneRequest.setDestinationPath(cloneName); + + JobResponse jobResponse = getNasFeignClient().cloneFile(getAuthHeader(), fileCloneRequest); + if (jobResponse == null || jobResponse.getJob() == null || + jobResponse.getJob().getUuid() == null || jobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException("Failed to initiate NFS clone create for " + cloneName); + } + Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException("NFS clone create job failed for " + cloneName); + } + return cloneName; + } - logger.info("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - svmName, flexVolName, snapshotName, ontapFilePath); + @Override + public void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, String cloneObjectUuid) { + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot clone delete"); + } + if (cloneName == null || cloneName.isEmpty()) { + throw new CloudRuntimeException("Clone file name is required for NFS snapshot clone delete"); + } + getNasFeignClient().deleteFile(getAuthHeader(), flexVolUuid, cloneName); + } - return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); + private boolean isFileAlreadyExistsConflict(FeignException e) { + if (e.status() == 409) { + return true; + } + String message = e.getMessage(); + if (message == null) { + return false; + } + String lower = message.toLowerCase(); + return lower.contains("already exists") + || lower.contains("entry exists") + || lower.contains("duplicate"); } } 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 5f1ac265fc50..ab9eb84826b9 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 @@ -29,8 +29,6 @@ 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.CliSnapshotRestoreRequest; -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; import org.apache.cloudstack.storage.service.model.CloudStackVolume; @@ -543,52 +541,143 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup return response; } /** - * Reverts a LUN to a snapshot using the ONTAP CLI-based snapshot file restore API. + * Reverts a destination LUN from source clone LUN data. * - *

ONTAP REST API (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}

- * - *

This method uses the CLI native API which is more reliable and works - * consistently for both NFS files and iSCSI LUNs.

- * - * @param snapshotName The ONTAP FlexVolume snapshot name - * @param flexVolUuid The FlexVolume UUID (not used in CLI API, kept for interface consistency) - * @param snapshotUuid The ONTAP snapshot UUID (not used in CLI API, kept for interface consistency) - * @param volumePath The LUN name (used to construct the path) - * @param lunUuid The LUN UUID (not used in CLI API, kept for interface consistency) - * @param flexVolName The FlexVolume name (required for CLI API) - * @return JobResponse for the async restore operation + *

Uses LUN PATCH clone-source semantics to overwrite the destination LUN + * contents while retaining clone metadata and strategy-driven path resolution.

*/ @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, - String lunUuid, String flexVolName) { - logger.trace("revertSnapshotForCloudStackVolume [iSCSI]: Restoring LUN [{}] from snapshot [{}] on FlexVol [{}]", + String flexVolName) { + logger.trace("revertSnapshotForCloudStackVolume [iSCSI]: Reverting LUN [{}] from clone [{}] on FlexVol [{}]", volumePath, snapshotName, flexVolName); if (snapshotName == null || snapshotName.isEmpty()) { - throw new CloudRuntimeException("Snapshot name is required for iSCSI snapshot revert"); + throw new CloudRuntimeException("Source clone LUN name is required for iSCSI snapshot revert"); + } + if (volumePath == null || volumePath.isEmpty()) { + throw new CloudRuntimeException("Destination LUN name is required for iSCSI snapshot revert"); } if (flexVolName == null || flexVolName.isEmpty()) { throw new CloudRuntimeException("FlexVolume name is required for iSCSI snapshot revert"); } - if (volumePath == null || volumePath.isEmpty()) { - throw new CloudRuntimeException("LUN path is required for iSCSI snapshot revert"); + if (snapshotUuid == null || snapshotUuid.isEmpty()) { + throw new CloudRuntimeException("Source clone LUN UUID is required for iSCSI snapshot revert"); + } + if (storage.getSvmName() == null || storage.getSvmName().isEmpty()) { + throw new CloudRuntimeException("SVM name is required for iSCSI snapshot revert"); } - String authHeader = getAuthHeader(); - String svmName = storage.getSvmName(); + String sourceLunPath = snapshotName.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? snapshotName : OntapStorageUtils.getLunName(flexVolName, snapshotName); + String destinationLunPath = volumePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? volumePath : OntapStorageUtils.getLunName(flexVolName, volumePath); - // Prepare the LUN path for ONTAP CLI API (ensure it starts with "/") - String ontapLunPath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + if (!sourceLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path for iSCSI snapshot revert: " + sourceLunPath); + } + if (!destinationLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid destination LUN path for iSCSI snapshot revert: " + destinationLunPath); + } - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapLunPath); + String authHeader = getAuthHeader(); + String destinationLunUuid = resolveLunUuidByName(authHeader, storage.getSvmName(), destinationLunPath); + Lun revertCloneRequest = new Lun(); + // PATCH /storage/luns/{uuid} rejects immutable destination attributes like svm.name. + // For restore, only provide clone source details and target the destination via UUID in URI. + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(sourceLunPath); + source.setUuid(snapshotUuid); + clone.setSource(source); + revertCloneRequest.setClone(clone); + + logger.debug("revertSnapshotForCloudStackVolume [iSCSI]: patch lun destinationUuid={} sourcePath={} sourceUuid={} destinationLun={}", + destinationLunUuid, sourceLunPath, snapshotUuid, destinationLunPath); + sanFeignClient.updateLun(authHeader, destinationLunUuid, revertCloneRequest); + } - logger.trace("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - svmName, flexVolName, snapshotName, ontapLunPath); + @Override + public String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid) { + if (sourceObjectUuid == null || sourceObjectUuid.isEmpty()) { + throw new CloudRuntimeException("Source LUN UUID is required for iSCSI snapshot clone create"); + } + if (sourcePath == null || sourcePath.isEmpty()) { + throw new CloudRuntimeException("Source LUN path is required for iSCSI snapshot clone create"); + } + if (flexVolName == null || flexVolName.isEmpty()) { + throw new CloudRuntimeException("FlexVolume name is required for iSCSI snapshot clone create"); + } + + String sourceLunPath = sourcePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? sourcePath : OntapStorageUtils.getLunName(flexVolName, sourcePath); + String cloneLunPath = cloneName.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? cloneName : OntapStorageUtils.getLunName(flexVolName, cloneName); + + if (!sourceLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path for iSCSI snapshot clone create: " + sourceLunPath); + } + if (!cloneLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid clone LUN path for iSCSI snapshot clone create: " + cloneLunPath); + } + if (storage.getSvmName() == null || storage.getSvmName().isEmpty()) { + throw new CloudRuntimeException("SVM name is mandatory for iSCSI snapshot clone create"); + } + + Lun cloneRequest = new Lun(); + cloneRequest.setName(cloneLunPath); + Svm svm = new Svm(); + svm.setName(storage.getSvmName()); + cloneRequest.setSvm(svm); + Lun.Location location = new Lun.Location(); + Lun.LocationVolume locationVolume = new Lun.LocationVolume(); + locationVolume.setName(flexVolName); + location.setVolume(locationVolume); + cloneRequest.setLocation(location); + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(sourceLunPath); + source.setUuid(sourceObjectUuid); + clone.setSource(source); + cloneRequest.setClone(clone); + + OntapResponse createCloneResponse = sanFeignClient.createLun(getAuthHeader(), true, cloneRequest); + if (createCloneResponse == null || createCloneResponse.getRecords() == null || createCloneResponse.getRecords().isEmpty()) { + throw new CloudRuntimeException("Failed to create iSCSI clone LUN for source path " + sourceLunPath); + } + String cloneUuid = createCloneResponse.getRecords().get(0).getUuid(); + if (cloneUuid == null || cloneUuid.isEmpty()) { + cloneUuid = resolveLunUuidByName(getAuthHeader(), storage.getSvmName(), cloneLunPath); + } + return cloneUuid; + } - return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); + @Override + public void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, String cloneObjectUuid) { + String cloneUuid = cloneObjectUuid; + if (cloneUuid == null || cloneUuid.isEmpty()) { + if (cloneName == null || cloneName.isEmpty()) { + throw new CloudRuntimeException("Clone LUN name is required to resolve UUID for iSCSI snapshot clone delete"); + } + if (flexVolName == null || flexVolName.isEmpty()) { + throw new CloudRuntimeException("FlexVolume name is required to resolve clone UUID for iSCSI snapshot clone delete"); + } + String cloneLunPath = cloneName.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? cloneName : OntapStorageUtils.getLunName(flexVolName, cloneName); + cloneUuid = resolveLunUuidByName(getAuthHeader(), storage.getSvmName(), cloneLunPath); + } + sanFeignClient.deleteLun(getAuthHeader(), cloneUuid, Map.of("allow_delete_while_mapped", "true")); + } + + private String resolveLunUuidByName(String authHeader, String svmName, String lunName) { + OntapResponse response = sanFeignClient.getLunResponse(authHeader, + Map.of(OntapStorageConstants.SVM_DOT_NAME, svmName, OntapStorageConstants.NAME, lunName)); + if (response == null || response.getRecords() == null || response.getRecords().isEmpty() + || response.getRecords().get(0).getUuid() == null || response.getRecords().get(0).getUuid().isEmpty()) { + throw new CloudRuntimeException("Failed to resolve destination LUN UUID for path: " + lunName); + } + return response.getRecords().get(0).getUuid(); } } 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 d0ea1783aa1d..dd2844852823 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 @@ -100,12 +100,17 @@ public class OntapStorageConstants { 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 ONTAP_SNAP_NAME = "ontap_snap_name"; + public static final String ONTAP_CLONE_ID = "ontap_clone_id"; + public static final String ONTAP_CLONE_NAME = "ontap_clone_name"; 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"; public static final String FILE_PATH = "file_path"; - public static final int MAX_SNAPSHOT_NAME_LENGTH = 64; + public static final int MAX_SNAPSHOT_NAME_LENGTH = 256; - /** vm_snapshot_details key for ONTAP FlexVolume-level VM snapshots. */ - public static final String ONTAP_FLEXVOL_SNAPSHOT = "ontapFlexVolSnapshot"; + /** vm_snapshot_details key for ONTAP clone-backed VM snapshot volume entries. */ + public static final String ONTAP_CLONE_SNAPSHOT_DETAIL = "ontapFlexVolSnapshot"; + /** @deprecated Use {@link #ONTAP_CLONE_SNAPSHOT_DETAIL}. Kept for compatibility. */ + @Deprecated + public static final String ONTAP_FLEXVOL_SNAPSHOT = ONTAP_CLONE_SNAPSHOT_DETAIL; } 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 596372edcf16..8ff931507588 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 @@ -154,4 +154,25 @@ public static String getLunName(String volName, String lunName) { return OntapStorageConstants.VOLUME_PATH_PREFIX + volName + OntapStorageConstants.SLASH + lunName; } + /** + * Uses CloudStack UI snapshot name as the preferred ONTAP clone name. + * If needed, normalizes just enough to satisfy ONTAP naming limits. + */ + public static String getOntapCloneName(String snapshotName) { + if (snapshotName == null || snapshotName.trim().isEmpty()) { + throw new InvalidParameterValueException("Snapshot name cannot be null or empty"); + } + String candidate = snapshotName.trim().replaceAll("[^a-zA-Z0-9_]", "_"); + if (!Character.isLetter(candidate.charAt(0))) { + candidate = "s_" + candidate; + } + if (candidate.length() > OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH) { + candidate = candidate.substring(0, OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH); + } + if (!isValidName(candidate)) { + throw new InvalidParameterValueException("Invalid ONTAP clone name derived from snapshot name: " + snapshotName); + } + return candidate; + } + } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java index a71df4c2e349..3e6bee299e0a 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java @@ -32,11 +32,6 @@ import org.apache.cloudstack.engine.subsystem.api.storage.VMSnapshotOptions; 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.CliSnapshotRestoreRequest; -import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot; -import org.apache.cloudstack.storage.feign.model.response.JobResponse; -import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import org.apache.cloudstack.storage.service.StorageStrategy; import org.apache.cloudstack.storage.service.model.ProtocolType; import org.apache.cloudstack.storage.to.VolumeObjectTO; @@ -72,36 +67,34 @@ import org.apache.cloudstack.storage.utils.OntapStorageConstants; /** - * VM Snapshot strategy for NetApp ONTAP managed storage using FlexVolume-level snapshots. + * VM Snapshot strategy for NetApp ONTAP managed storage using clone artifacts. * *

This strategy handles VM-level (instance) snapshots for VMs whose volumes - * reside on ONTAP managed primary storage. Instead of creating per-file clones - * (the old approach), it takes ONTAP FlexVolume-level snapshots via the - * ONTAP REST API ({@code POST /api/storage/volumes/{uuid}/snapshots}).

+ * reside on ONTAP managed primary storage by creating per-volume clone artifacts + * (file clones for NAS, LUN clones for SAN).

* - *

Key Advantage:

- *

When multiple CloudStack disks (ROOT + DATA) reside on the same ONTAP - * FlexVolume, a single FlexVolume snapshot atomically captures all of them. - * This is both faster and more storage-efficient than per-file clones.

+ *

Key Behavior:

+ *

Each CloudStack volume in the VM snapshot is represented by a dedicated ONTAP + * clone artifact. The workflow stores one detail row per CloudStack volume so + * revert/delete can operate precisely per volume.

* *

Flow:

*
    *
  1. Group all VM volumes by their parent FlexVolume UUID
  2. *
  3. Freeze the VM via QEMU guest agent ({@code fsfreeze}) — if quiesce requested
  4. - *
  5. For each unique FlexVolume, create one ONTAP snapshot
  6. + *
  7. For each volume, create a protocol-specific clone artifact
  8. *
  9. Thaw the VM
  10. - *
  11. Record FlexVolume → snapshot UUID mappings in {@code vm_snapshot_details}
  12. + *
  13. Record clone artifact metadata in {@code vm_snapshot_details}
  14. *
* *

Metadata in vm_snapshot_details:

- *

Each FlexVolume snapshot is stored as a detail row with: + *

Each clone artifact is stored as a detail row with: *

    - *
  • name = {@value OntapStorageConstants#ONTAP_FLEXVOL_SNAPSHOT}
  • - *
  • value = {@code "::::::::::"}
  • + *
  • name = {@value OntapStorageConstants#ONTAP_CLONE_SNAPSHOT_DETAIL}
  • + *
  • value = {@code "::::::::::"}
  • *
- * One row is persisted per CloudStack volume (not per FlexVolume) so that the - * revert operation can restore individual files/LUNs using the ONTAP Snapshot - * File Restore API ({@code POST /api/storage/volumes/{vol}/snapshots/{snap}/files/{path}/restore}).

+ * One row is persisted per CloudStack volume so that the revert operation can + * restore from clone artifacts using protocol-specific APIs.

* *

Strategy Selection:

*

Returns {@code StrategyPriority.HIGHEST} when:

@@ -115,7 +108,7 @@ public class OntapVMSnapshotStrategy extends StorageVMSnapshotStrategy { private static final Logger logger = LogManager.getLogger(OntapVMSnapshotStrategy.class); - /** Separator used in the vm_snapshot_details value to delimit FlexVol UUID, snapshot UUID, snapshot name, and pool ID. */ + /** Separator used in vm_snapshot_details values for clone metadata serialization. */ static final String DETAIL_SEPARATOR = "::"; @Inject @@ -139,20 +132,15 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { // For existing (non-Allocated) snapshots, check if we created them if (!VMSnapshot.State.Allocated.equals(vmSnapshotVO.getState())) { - // Check for our FlexVolume snapshot details first - List flexVolDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT); - if (CollectionUtils.isNotEmpty(flexVolDetails)) { + // Check for our clone snapshot details first + List cloneSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_CLONE_SNAPSHOT_DETAIL); + if (CollectionUtils.isNotEmpty(cloneSnapshotDetails)) { // Verify the volumes are still on ONTAP storage if (allVolumesOnOntapManagedStorage(vmSnapshot.getVmId())) { return StrategyPriority.HIGHEST; } return StrategyPriority.CANT_HANDLE; } - // Also check legacy STORAGE_SNAPSHOT details for backward compatibility - List legacyDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), STORAGE_SNAPSHOT); - if (CollectionUtils.isNotEmpty(legacyDetails) && allVolumesOnOntapManagedStorage(vmSnapshot.getVmId())) { - return StrategyPriority.HIGHEST; - } return StrategyPriority.CANT_HANDLE; } @@ -171,7 +159,7 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { @Override public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMemory) { - // ONTAP FlexVolume snapshots only support disk-only (crash-consistent) snapshots. + // ONTAP clone-backed snapshots only support disk-only (crash-consistent) snapshots. // Memory snapshots (snapshotMemory=true) are not supported because: // 1. ONTAP snapshots capture disk state only, not VM memory // 2. Allowing memory snapshots would require falling back to libvirt snapshots, @@ -246,21 +234,19 @@ boolean allVolumesOnOntapManagedStorage(long vmId) { } // ────────────────────────────────────────────────────────────────────────── - // Take VM Snapshot (FlexVolume-level) + // Take VM Snapshot (clone-backed) // ────────────────────────────────────────────────────────────────────────── /** - * Takes a VM-level snapshot by freezing the VM, creating ONTAP FlexVolume-level - * snapshots (one per unique FlexVolume), and then thawing the VM. + * Takes a VM-level snapshot by freezing the VM, creating ONTAP clone artifacts + * (one per CloudStack volume), and then thawing the VM. * - *

Volumes are grouped by their parent FlexVolume UUID (from storage pool details). - * For each unique FlexVolume, exactly one ONTAP snapshot is created via - * {@code POST /api/storage/volumes/{uuid}/snapshots}. This means if a VM has - * ROOT and DATA disks on the same FlexVolume, only one snapshot is created.

+ *

Volumes are grouped by FlexVolume UUID for efficient metadata lookup, but a + * dedicated clone artifact is created per CloudStack volume.

* *

Memory Snapshots Not Supported: This strategy only supports disk-only * (crash-consistent) snapshots. Memory snapshots (snapshotmemory=true) are rejected - * with a clear error message. This is because ONTAP FlexVolume snapshots capture disk + * with a clear error message. This is because ONTAP clone-backed snapshots capture disk * state only, and allowing mixed snapshot chains (ONTAP disk + libvirt memory) would * cause issues during revert operations.

* @@ -286,8 +272,8 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { FreezeThawVMAnswer thawAnswer = null; long startFreeze = 0; - // Track which FlexVolume snapshots were created (for rollback) - List createdSnapshots = new ArrayList<>(); + // Track which clone artifacts were created (for rollback) + List createdSnapshots = new ArrayList<>(); boolean result = false; try { @@ -317,7 +303,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { if (!vmIsRunning) { logger.info("takeVMSnapshot: VM [{}] is in state [{}] (not Running). Skipping freeze/thaw - " + - "FlexVolume snapshot will be taken directly.", userVm.getInstanceName(), userVm.getState()); + "clone artifacts will be created directly.", userVm.getInstanceName(), userVm.getState()); } else if (quiesceVm) { logger.info("takeVMSnapshot: Quiesce option is enabled for ONTAP VM Snapshot of VM [{}]. " + "VM file systems will be frozen/thawed for application-consistent snapshots.", userVm.getInstanceName()); @@ -350,8 +336,6 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { // ── Group volumes by FlexVolume UUID ── Map flexVolGroups = groupVolumesByFlexVol(volumeTOs); - logger.info("takeVMSnapshot: VM [{}] has {} volumes across {} unique FlexVolume(s)", - userVm.getInstanceName(), volumeTOs.size(), flexVolGroups.size()); // ── Step 1: Freeze the VM (only if quiescing is requested AND VM is running) ── if (shouldFreezeThaw) { @@ -375,7 +359,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { userVm.getInstanceName(), quiesceVm, vmIsRunning); } - // ── Step 2: Create FlexVolume-level snapshots ── + // ── Step 2: Create clone-backed VM snapshot entries ── try { String snapshotNameBase = buildSnapshotName(vmSnapshot); @@ -386,43 +370,31 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { // Build storage strategy from pool details to get the feign client StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(groupInfo.poolDetails); - SnapshotFeignClient snapshotClient = storageStrategy.getSnapshotFeignClient(); String authHeader = storageStrategy.getAuthHeader(); - - // Use the same snapshot name for all FlexVolumes in this VM snapshot - // (each FlexVolume gets its own independent snapshot with this name) - FlexVolSnapshot snapshotRequest = new FlexVolSnapshot(snapshotNameBase, - "CloudStack VM snapshot " + vmSnapshot.getName() + " for VM " + userVm.getInstanceName()); - - logger.info("takeVMSnapshot: Creating ONTAP FlexVolume snapshot [{}] on FlexVol UUID [{}] covering {} volume(s)", - snapshotNameBase, flexVolUuid, groupInfo.volumeIds.size()); - - JobResponse jobResponse = snapshotClient.createSnapshot(authHeader, flexVolUuid, snapshotRequest); - if (jobResponse == null || jobResponse.getJob() == null) { - throw new CloudRuntimeException("Failed to initiate FlexVolume snapshot on FlexVol UUID [" + flexVolUuid + "]"); - } - - // Poll for job completion - Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); - if (!jobSucceeded) { - throw new CloudRuntimeException("FlexVolume snapshot job failed on FlexVol UUID [" + flexVolUuid + "]"); - } - - // Retrieve the created snapshot UUID by name - String snapshotUuid = resolveSnapshotUuid(snapshotClient, authHeader, flexVolUuid, snapshotNameBase); - String protocol = groupInfo.poolDetails.get(OntapStorageConstants.PROTOCOL); - // Create one detail per CloudStack volume in this FlexVol group (for single-file restore during revert) + // Create one clone per CloudStack volume and persist detail for protocol-specific revert. for (Long volumeId : groupInfo.volumeIds) { String volumePath = resolveVolumePathOnOntap(volumeId, protocol, groupInfo.poolDetails); - FlexVolSnapshotDetail detail = new FlexVolSnapshotDetail( - flexVolUuid, snapshotUuid, snapshotNameBase, volumePath, groupInfo.poolId, protocol); + String cloneName = buildPerVolumeCloneName(snapshotNameBase, vmSnapshot.getId(), volumeId); + String sourceObjectUuid = null; + if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { + VolumeDetailVO lunDetail = volumeDetailsDao.findDetail(volumeId, OntapStorageConstants.LUN_DOT_UUID); + String sourceLunUuid = lunDetail != null ? lunDetail.getValue() : null; + if (sourceLunUuid == null || sourceLunUuid.isEmpty()) { + throw new CloudRuntimeException("Source LUN UUID missing for volume " + volumeId); + } + sourceObjectUuid = sourceLunUuid; + } + String cloneUuid = storageStrategy.createSnapshotClone(flexVolUuid, + groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME), volumePath, cloneName, sourceObjectUuid); + CSVolSnapshotDetail detail = new CSVolSnapshotDetail( + flexVolUuid, cloneUuid, cloneName, volumePath, groupInfo.poolId, protocol); createdSnapshots.add(detail); } - logger.info("takeVMSnapshot: ONTAP FlexVolume snapshot [{}] (uuid={}) on FlexVol [{}] completed in {} ms. Covers volumes: {}", - snapshotNameBase, snapshotUuid, flexVolUuid, + logger.info("takeVMSnapshot: Clone-backed VM snapshot [{}] on FlexVol [{}] completed in {} ms. Covers volumes: {}", + snapshotNameBase, flexVolUuid, TimeUnit.MILLISECONDS.convert(System.nanoTime() - startSnapshot, TimeUnit.NANOSECONDS), groupInfo.volumeIds); } @@ -445,10 +417,10 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } } - // ── Step 4: Persist FlexVolume snapshot details (one row per CloudStack volume) ── - for (FlexVolSnapshotDetail detail : createdSnapshots) { + // ── Step 4: Persist clone snapshot details (one row per CloudStack volume) ── + for (CSVolSnapshotDetail detail : createdSnapshots) { vmSnapshotDetailsDao.persist(new VMSnapshotDetailsVO( - vmSnapshot.getId(), OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT, detail.toString(), true)); + vmSnapshot.getId(), OntapStorageConstants.ONTAP_CLONE_SNAPSHOT_DETAIL, detail.toString(), true)); } // ── Step 5: Finalize via parent processAnswer ── @@ -456,7 +428,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { answer.setVolumeTOs(volumeTOs); processAnswer(vmSnapshotVO, userVm, answer, null); - logger.info("takeVMSnapshot: ONTAP FlexVolume VM Snapshot [{}] created successfully for VM [{}] ({} FlexVol snapshot(s))", + logger.info("takeVMSnapshot: ONTAP VM Snapshot [{}] created successfully for VM [{}] ({} snapshot(s))", vmSnapshot.getName(), userVm.getInstanceName(), createdSnapshots.size()); long newChainSize = 0; @@ -476,15 +448,19 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { } catch (AgentUnavailableException e) { logger.error("takeVMSnapshot: ONTAP VM Snapshot [{}] failed, agent unavailable: {}", vmSnapshot.getName(), e.getMessage()); throw new CloudRuntimeException("Creating Instance Snapshot: " + vmSnapshot.getName() + " failed: " + e.getMessage()); - } finally { + } catch (Exception e) { + logger.error("takeVMSnapshot: ONTAP VM Snapshot [{}] failed, with exception: {}", vmSnapshot.getName(), e.getMessage()); + throw new CloudRuntimeException("Creating Instance Snapshot: " + vmSnapshot.getName() + " failed: " + e.getMessage()); + } + finally { if (!result) { - // Rollback all FlexVolume snapshots created so far (deduplicate by FlexVol+Snapshot) + // Rollback all created clone artifacts so far (deduplicate by FlexVol+Clone) Map rolledBack = new HashMap<>(); - for (FlexVolSnapshotDetail detail : createdSnapshots) { + for (CSVolSnapshotDetail detail : createdSnapshots) { String dedupeKey = detail.flexVolUuid + "::" + detail.snapshotUuid; if (!rolledBack.containsKey(dedupeKey)) { try { - rollbackFlexVolSnapshot(detail); + rollbackCloudStackVolSnapshot(detail); rolledBack.put(dedupeKey, Boolean.TRUE); } catch (Exception rollbackEx) { logger.error("takeVMSnapshot: Failed to rollback FlexVol snapshot [{}] on FlexVol [{}]: {}", @@ -507,7 +483,7 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { try { List vmSnapshotDetails = vmSnapshotDetailsDao.listDetails(vmSnapshot.getId()); for (VMSnapshotDetailsVO detail : vmSnapshotDetails) { - if (OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT.equals(detail.getName())) { + if (OntapStorageConstants.ONTAP_CLONE_SNAPSHOT_DETAIL.equals(detail.getName())) { vmSnapshotDetailsDao.remove(detail.getId()); } } @@ -545,16 +521,10 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { DeleteVMSnapshotCommand deleteSnapshotCommand = new DeleteVMSnapshotCommand(vmInstanceName, vmSnapshotTO, volumeTOs, guestOS.getDisplayName()); - // Check for FlexVolume snapshots (new approach) - List flexVolDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT); - if (CollectionUtils.isNotEmpty(flexVolDetails)) { - deleteFlexVolSnapshots(flexVolDetails); - } - - // Also handle legacy STORAGE_SNAPSHOT details (backward compatibility) - List legacyDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), STORAGE_SNAPSHOT); - if (CollectionUtils.isNotEmpty(legacyDetails)) { - deleteDiskSnapshot(vmSnapshot); + // Check for clone snapshot details + List cloneSnapshotDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_CLONE_SNAPSHOT_DETAIL); + if (CollectionUtils.isNotEmpty(cloneSnapshotDetails)) { + deleteCloneSnapshotArtifacts(cloneSnapshotDetails); } processAnswer(vmSnapshotVO, userVm, new DeleteVMSnapshotAnswer(deleteSnapshotCommand, volumeTOs), null); @@ -600,16 +570,12 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { RevertToVMSnapshotCommand revertToSnapshotCommand = new RevertToVMSnapshotCommand(vmInstanceName, userVm.getUuid(), vmSnapshotTO, volumeTOs, guestOS.getDisplayName()); - // Check for FlexVolume snapshots (new approach) - List flexVolDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT); - if (CollectionUtils.isNotEmpty(flexVolDetails)) { - revertFlexVolSnapshots(flexVolDetails); - } - - // Also handle legacy STORAGE_SNAPSHOT details (backward compatibility) - List legacyDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), STORAGE_SNAPSHOT); - if (CollectionUtils.isNotEmpty(legacyDetails)) { - revertDiskSnapshot(vmSnapshot); + // Revert clone-backed snapshot artifacts per volume: + // - NFS: patch file(source=clone, destination=live file, overwrite=true) + // - iSCSI: patch LUN (clone.source=clone LUN, destination=live LUN) + List cloneDetails = vmSnapshotDetailsDao.findDetails(vmSnapshot.getId(), OntapStorageConstants.ONTAP_CLONE_SNAPSHOT_DETAIL); + if (CollectionUtils.isNotEmpty(cloneDetails)) { + revertCloneBackedSnapshots(cloneDetails); } RevertToVMSnapshotAnswer answer = new RevertToVMSnapshotAnswer(revertToSnapshotCommand, true, ""); @@ -632,7 +598,7 @@ public boolean revertVMSnapshot(VMSnapshot vmSnapshot) { } // ────────────────────────────────────────────────────────────────────────── - // FlexVolume Snapshot Helpers + // Clone Snapshot Helpers // ────────────────────────────────────────────────────────────────────────── /** @@ -672,27 +638,15 @@ Map groupVolumesByFlexVol(List volumeT * Format: {@code vmsnap__} */ String buildSnapshotName(VMSnapshot vmSnapshot) { - String name = "vmsnap_" + vmSnapshot.getId() + "_" + System.currentTimeMillis(); - // ONTAP snapshot names: max 256 chars, must start with letter, only alphanumeric and underscores - if (name.length() > OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH) { - name = name.substring(0, OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH); - } - return name; + return OntapStorageUtils.getOntapCloneName(vmSnapshot.getName()); } /** - * Resolves the UUID of a newly created FlexVolume snapshot by name. + * Builds a deterministic per-volume clone name for VM snapshot workflows. + * Keeps VM snapshot name as base while preventing collisions across ROOT/DATA volumes. */ - String resolveSnapshotUuid(SnapshotFeignClient client, String authHeader, - String flexVolUuid, String snapshotName) { - Map queryParams = new HashMap<>(); - queryParams.put("name", snapshotName); - OntapResponse response = client.getSnapshots(authHeader, flexVolUuid, queryParams); - if (response == null || response.getRecords() == null || response.getRecords().isEmpty()) { - throw new CloudRuntimeException("Could not find FlexVolume snapshot [" + snapshotName + - "] on FlexVol [" + flexVolUuid + "] after creation"); - } - return response.getRecords().get(0).getUuid(); + String buildPerVolumeCloneName(String snapshotNameBase, Long vmSnapshotId, Long volumeId) { + return OntapStorageUtils.getOntapCloneName(snapshotNameBase + "_s" + vmSnapshotId + "_v" + volumeId); } /** @@ -730,59 +684,52 @@ String resolveVolumePathOnOntap(Long volumeId, String protocol, Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(detail.poolId); StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - SnapshotFeignClient client = storageStrategy.getSnapshotFeignClient(); - String authHeader = storageStrategy.getAuthHeader(); - - logger.info("rollbackFlexVolSnapshot: Rolling back FlexVol snapshot [{}] (uuid={}) on FlexVol [{}]", - detail.snapshotName, detail.snapshotUuid, detail.flexVolUuid); - - JobResponse jobResponse = client.deleteSnapshot(authHeader, detail.flexVolUuid, detail.snapshotUuid); - if (jobResponse != null && jobResponse.getJob() != null) { - storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 10, 2000); - } + storageStrategy.deleteSnapshotClone(detail.flexVolUuid, poolDetails.get(OntapStorageConstants.VOLUME_NAME), + detail.snapshotName, detail.snapshotUuid); } catch (Exception e) { - logger.error("rollbackFlexVolSnapshot: Rollback of FlexVol snapshot failed: {}", e.getMessage(), e); + logger.error("rollbackCloudStackVolSnapshot: Rollback of CloudStack Vol snapshot failed: {}", e.getMessage(), e); } } /** - * Deletes all FlexVolume snapshots associated with a VM snapshot. + * Deletes all CS-Volume snapshots associated with a VM snapshot. * *

Since there is one detail row per CloudStack volume, multiple rows may reference - * the same FlexVol + snapshot combination. This method deduplicates to delete each - * underlying ONTAP snapshot only once.

+ * the respective CS Vol + snapshot combination.

*/ - void deleteFlexVolSnapshots(List flexVolDetails) { + void deleteCloneSnapshotArtifacts(List cloneSnapshotDetails) { // Track which FlexVol+Snapshot pairs have already been deleted Map deletedSnapshots = new HashMap<>(); - for (VMSnapshotDetailsVO detailVO : flexVolDetails) { - FlexVolSnapshotDetail detail = FlexVolSnapshotDetail.parse(detailVO.getValue()); + for (VMSnapshotDetailsVO detailVO : cloneSnapshotDetails) { + CSVolSnapshotDetail detail = CSVolSnapshotDetail.parse(detailVO.getValue()); String dedupeKey = detail.flexVolUuid + "::" + detail.snapshotUuid; // Only delete the ONTAP snapshot once per FlexVol+Snapshot pair if (!deletedSnapshots.containsKey(dedupeKey)) { Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(detail.poolId); StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - SnapshotFeignClient client = storageStrategy.getSnapshotFeignClient(); - String authHeader = storageStrategy.getAuthHeader(); - logger.info("deleteFlexVolSnapshots: Deleting ONTAP FlexVol snapshot [{}] (uuid={}) on FlexVol [{}]", - detail.snapshotName, detail.snapshotUuid, detail.flexVolUuid); - - JobResponse jobResponse = client.deleteSnapshot(authHeader, detail.flexVolUuid, detail.snapshotUuid); - if (jobResponse != null && jobResponse.getJob() != null) { - storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); + try { + storageStrategy.deleteSnapshotClone(detail.flexVolUuid, poolDetails.get(OntapStorageConstants.VOLUME_NAME), + detail.snapshotName, detail.snapshotUuid); + } catch (Exception e) { + if (isSnapshotAlreadyMissing(e)) { + logger.warn("deleteCloneSnapshotArtifacts: Clone [{}] on FlexVol [{}] is already missing. " + + "Treating as success.", detail.snapshotName, detail.flexVolUuid); + } else { + throw e; + } } deletedSnapshots.put(dedupeKey, Boolean.TRUE); - logger.info("deleteFlexVolSnapshots: Deleted ONTAP FlexVol snapshot [{}] on FlexVol [{}]", detail.snapshotName, detail.flexVolUuid); + logger.info("deleteCloneSnapshotArtifacts: Deleted clone [{}] on FlexVol [{}]", detail.snapshotName, detail.flexVolUuid); } // Always remove the DB detail row @@ -790,72 +737,61 @@ void deleteFlexVolSnapshots(List flexVolDetails) { } } + private boolean isSnapshotAlreadyMissing(Exception e) { + String message = e.getMessage(); + if (message == null) { + return false; + } + String lower = message.toLowerCase(); + return lower.contains("entry doesn't exist") + || lower.contains("entry does not exist") + || lower.contains("not found") + || lower.contains("404"); + } + /** - * Reverts all volumes of a VM snapshot using ONTAP CLI-based Snapshot File Restore. + * Reverts all volumes of a VM snapshot using clone-backed restore operations. * - *

Instead of restoring the entire FlexVolume to a snapshot (which would affect - * other VMs/files on the same FlexVol), this method restores only the individual - * files or LUNs belonging to this VM using the dedicated ONTAP CLI snapshot file - * restore API:

+ *

Each persisted detail row represents one volume and points to the clone artifact + * created during VM snapshot creation. Revert copies from the clone artifact back to + * the original volume object.

* - *

{@code POST /api/private/cli/volume/snapshot/restore-file}

- * - *

For each persisted detail row (one per CloudStack volume):

*
    - *
  • NFS: restores {@code } from the snapshot to the live volume
  • - *
  • iSCSI: restores {@code } from the snapshot to the live volume
  • + *
  • NFS: clone file from snapshot clone file path to original file path, with overwrite
  • + *
  • iSCSI: patch destination LUN with clone source ({@code clone.source.name/uuid})
  • *
*/ - void revertFlexVolSnapshots(List flexVolDetails) { - for (VMSnapshotDetailsVO detailVO : flexVolDetails) { - FlexVolSnapshotDetail detail = FlexVolSnapshotDetail.parse(detailVO.getValue()); + void revertCloneBackedSnapshots(List cloneDetails) { + for (VMSnapshotDetailsVO detailVO : cloneDetails) { + CSVolSnapshotDetail detail = CSVolSnapshotDetail.parse(detailVO.getValue()); if (detail.volumePath == null || detail.volumePath.isEmpty()) { // Legacy detail row without volumePath – cannot do single-file restore - logger.warn("revertFlexVolSnapshots: FlexVol snapshot detail for FlexVol [{}] has no volumePath (legacy format). " + + logger.warn("revertCloneBackedSnapshots: Snapshot detail for FlexVol [{}] has no volumePath (legacy format). " + "Skipping single-file restore for this entry.", detail.flexVolUuid); continue; } Map poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(detail.poolId); StorageStrategy storageStrategy = OntapStorageUtils.getStrategyByStoragePoolDetails(poolDetails); - SnapshotFeignClient snapshotClient = storageStrategy.getSnapshotFeignClient(); - String authHeader = storageStrategy.getAuthHeader(); - - // Get SVM name and FlexVolume name from pool details - String svmName = poolDetails.get(OntapStorageConstants.SVM_NAME); String flexVolName = poolDetails.get(OntapStorageConstants.VOLUME_NAME); - - if (svmName == null || svmName.isEmpty()) { - throw new CloudRuntimeException("SVM name not found in pool details for pool [" + detail.poolId + "]"); - } if (flexVolName == null || flexVolName.isEmpty()) { throw new CloudRuntimeException("FlexVolume name not found in pool details for pool [" + detail.poolId + "]"); } - // The path must start with "/" for the ONTAP CLI API - String ontapFilePath = detail.volumePath.startsWith("/") ? detail.volumePath : "/" + detail.volumePath; - - logger.info("revertFlexVolSnapshots: Restoring volume [{}] from FlexVol snapshot [{}] on FlexVol [{}] (protocol={})", - ontapFilePath, detail.snapshotName, flexVolName, detail.protocol); - - // Use CLI-based restore API: POST /api/private/cli/volume/snapshot/restore-file - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, detail.snapshotName, ontapFilePath); - - JobResponse jobResponse = snapshotClient.restoreFileFromSnapshotCli(authHeader, restoreRequest); - - if (jobResponse != null && jobResponse.getJob() != null) { - Boolean success = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2000); - if (!success) { - throw new CloudRuntimeException("Snapshot file restore failed for volume path [" + - ontapFilePath + "] from snapshot [" + detail.snapshotName + - "] on FlexVol [" + flexVolName + "]"); - } + logger.info("revertCloneBackedSnapshots: Reverting volume [{}] using clone source [{}] on FlexVol [{}] (protocol={})", + detail.volumePath, detail.snapshotName, flexVolName, detail.protocol); + try { + storageStrategy.revertSnapshotForCloudStackVolume( + detail.snapshotName, detail.flexVolUuid, detail.snapshotUuid, detail.volumePath, flexVolName); + } catch (Exception e) { + logger.error("revertCloneBackedSnapshots: Revert of FlexVol snapshot failed: {}", e.getMessage(), e); + throw new CloudRuntimeException("Failed to revert volume [" + detail.volumePath + "] from clone [" + + detail.snapshotName + "] on FlexVol [" + flexVolName + "]: " + e.getMessage(), e); } - logger.info("revertFlexVolSnapshots: Successfully restored volume [{}] from snapshot [{}] on FlexVol [{}]", - ontapFilePath, detail.snapshotName, flexVolName); + logger.info("revertCloneBackedSnapshots: Successfully reverted volume [{}] from clone [{}] on FlexVol [{}]", + detail.volumePath, detail.snapshotName, flexVolName); } } @@ -878,7 +814,7 @@ static class FlexVolGroupInfo { } /** - * Holds the metadata for a single volume's FlexVolume snapshot entry (used during create and for + * Holds the metadata for a single volume's clone snapshot entry (used during create and for * serialization/deserialization to/from vm_snapshot_details). * *

One row is persisted per CloudStack volume. Multiple volumes may share the same @@ -886,7 +822,7 @@ static class FlexVolGroupInfo { * *

Serialized format: {@code "::::::::::"}

*/ - static class FlexVolSnapshotDetail { + static class CSVolSnapshotDetail { final String flexVolUuid; final String snapshotUuid; final String snapshotName; @@ -896,8 +832,8 @@ static class FlexVolSnapshotDetail { /** Storage protocol: NFS3, ISCSI, etc. */ final String protocol; - FlexVolSnapshotDetail(String flexVolUuid, String snapshotUuid, String snapshotName, - String volumePath, long poolId, String protocol) { + CSVolSnapshotDetail(String flexVolUuid, String snapshotUuid, String snapshotName, + String volumePath, long poolId, String protocol) { this.flexVolUuid = flexVolUuid; this.snapshotUuid = snapshotUuid; this.snapshotName = snapshotName; @@ -907,18 +843,18 @@ static class FlexVolSnapshotDetail { } /** - * Parses a vm_snapshot_details value string back into a FlexVolSnapshotDetail. + * Parses a vm_snapshot_details value string back into a CSVolSnapshotDetail. */ - static FlexVolSnapshotDetail parse(String value) { + static CSVolSnapshotDetail parse(String value) { String[] parts = value.split(DETAIL_SEPARATOR); if (parts.length == 4) { // Legacy format without volumePath and protocol: flexVolUuid::snapshotUuid::snapshotName::poolId - return new FlexVolSnapshotDetail(parts[0], parts[1], parts[2], null, Long.parseLong(parts[3]), null); + return new CSVolSnapshotDetail(parts[0], parts[1], parts[2], null, Long.parseLong(parts[3]), null); } if (parts.length != 6) { throw new CloudRuntimeException("Invalid ONTAP FlexVol snapshot detail format: " + value); } - return new FlexVolSnapshotDetail(parts[0], parts[1], parts[2], parts[3], Long.parseLong(parts[4]), parts[5]); + return new CSVolSnapshotDetail(parts[0], parts[1], parts[2], parts[3], Long.parseLong(parts[4]), parts[5]); } @Override 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 b535217fd235..8e104827a68e 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 @@ -25,23 +25,30 @@ import com.cloud.storage.Storage; import com.cloud.storage.VolumeVO; import com.cloud.storage.VolumeDetailVO; +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.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.SnapshotInfo; 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.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.client.NASFeignClient; +import org.apache.cloudstack.storage.feign.client.SANFeignClient; import org.apache.cloudstack.storage.feign.model.Igroup; import org.apache.cloudstack.storage.feign.model.Lun; +import org.apache.cloudstack.storage.service.StorageStrategy; 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.OntapStorageConstants; import org.apache.cloudstack.storage.utils.OntapStorageUtils; import org.junit.jupiter.api.BeforeEach; @@ -56,6 +63,7 @@ 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.VOLUME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -67,10 +75,12 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -89,6 +99,9 @@ class OntapPrimaryDatastoreDriverTest { @Mock private VolumeDetailsDao volumeDetailsDao; + @Mock + private SnapshotDetailsDao snapshotDetailsDao; + @Mock private DataStore dataStore; @@ -107,6 +120,18 @@ class OntapPrimaryDatastoreDriverTest { @Mock private UnifiedSANStrategy sanStrategy; + @Mock + private StorageStrategy storageStrategy; + + @Mock + private NASFeignClient nasFeignClient; + + @Mock + private SANFeignClient sanFeignClient; + + @Mock + private SnapshotInfo snapshotInfo; + @Mock private AsyncCompletionCallback createCallback; @@ -564,4 +589,178 @@ void testCanProvideStorageStats_ReturnsFalse() { void testCanProvideVolumeStats_ReturnsFalse() { assertFalse(driver.canProvideVolumeStats()); } + + @Test + void testTakeSnapshot_NfsCloneSuccess() { + storagePoolDetails.put(OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name()); + storagePoolDetails.put(OntapStorageConstants.VOLUME_UUID, "flexvol-uuid-1"); + storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "flexvol1"); + storagePoolDetails.put(OntapStorageConstants.SVM_NAME, "svm1"); + storagePoolDetails.put(OntapStorageConstants.USERNAME, "admin"); + storagePoolDetails.put(OntapStorageConstants.PASSWORD, "pass"); + storagePoolDetails.put(OntapStorageConstants.STORAGE_IP, "10.0.0.1"); + storagePoolDetails.put(OntapStorageConstants.SIZE, "1024"); + + when(snapshotInfo.getId()).thenReturn(500L); + when(snapshotInfo.getName()).thenReturn("UI Snapshot Name"); + when(snapshotInfo.getBaseVolume()).thenReturn(volumeInfo); + SnapshotObjectTO snapshotObjectTO = mock(SnapshotObjectTO.class); + when(snapshotInfo.getTO()).thenReturn(snapshotObjectTO); + when(volumeInfo.getId()).thenReturn(100L); + when(volumeVO.getId()).thenReturn(100L); + when(volumeVO.getPoolId()).thenReturn(1L); + when(volumeVO.getPath()).thenReturn("vol-100.qcow2"); + when(volumeDao.findById(100L)).thenReturn(volumeVO); + when(storagePoolDao.findById(1L)).thenReturn(storagePool); + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + when(storageStrategy.createSnapshotClone(eq("flexvol-uuid-1"), eq("flexvol1"), + eq("vol-100.qcow2"), eq("UI_Snapshot_Name"), isNull())).thenReturn("UI_Snapshot_Name"); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + utilityMock.when(() -> OntapStorageUtils.getOntapCloneName("UI Snapshot Name")) + .thenReturn("UI_Snapshot_Name"); + + driver.takeSnapshot(snapshotInfo, createCallback); + + verify(storageStrategy).createSnapshotClone(eq("flexvol-uuid-1"), eq("flexvol1"), + eq("vol-100.qcow2"), eq("UI_Snapshot_Name"), isNull()); + verify(snapshotDetailsDao, atLeastOnce()).persist(any(SnapshotDetailsVO.class)); + verify(createCallback).complete(any(CreateCmdResult.class)); + } + } + + @Test + void testRevertSnapshot_UsesCloneMetadata() { + when(snapshotInfo.getId()).thenReturn(500L); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.BASE_ONTAP_FV_ID)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.BASE_ONTAP_FV_ID, "flexvol-uuid-1", false)); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.ONTAP_CLONE_ID)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.ONTAP_CLONE_ID, "clone-lun-uuid-1", false)); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.ONTAP_CLONE_NAME)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.ONTAP_CLONE_NAME, "UI_Snapshot_Name", false)); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.VOLUME_PATH)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.VOLUME_PATH, "dest-lun-1", false)); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.PRIMARY_POOL_ID)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false)); + when(snapshotDetailsDao.findDetail(500L, OntapStorageConstants.PROTOCOL)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.PROTOCOL, ProtocolType.ISCSI.name(), false)); + + storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "flexvol1"); + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + doNothing().when(storageStrategy).revertSnapshotForCloudStackVolume(anyString(), anyString(), anyString(), anyString(), anyString()); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + + driver.revertSnapshot(snapshotInfo, snapshotInfo, commandCallback); + + verify(storageStrategy).revertSnapshotForCloudStackVolume( + eq("UI_Snapshot_Name"), eq("flexvol-uuid-1"), eq("clone-lun-uuid-1"), + eq("dest-lun-1"), eq("flexvol1")); + verify(commandCallback).complete(any(CommandResult.class)); + } + } + + @Test + void testRevertSnapshot_FallbacksToLegacySnapshotNameWhenCloneNameMissing() { + when(snapshotInfo.getId()).thenReturn(501L); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.BASE_ONTAP_FV_ID)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.BASE_ONTAP_FV_ID, "flexvol-uuid-1", false)); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.ONTAP_CLONE_ID)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.ONTAP_CLONE_ID, "clone-lun-uuid-2", false)); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.ONTAP_CLONE_NAME)).thenReturn(null); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.ONTAP_SNAP_NAME)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.ONTAP_SNAP_NAME, "Legacy_UI_Snapshot", false)); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.VOLUME_PATH)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.VOLUME_PATH, "dest-lun-1", false)); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.PRIMARY_POOL_ID)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false)); + when(snapshotDetailsDao.findDetail(501L, OntapStorageConstants.PROTOCOL)) + .thenReturn(new SnapshotDetailsVO(501L, OntapStorageConstants.PROTOCOL, ProtocolType.ISCSI.name(), false)); + + storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "flexvol1"); + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + + doNothing().when(storageStrategy).revertSnapshotForCloudStackVolume(anyString(), anyString(), anyString(), anyString(), anyString()); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + + driver.revertSnapshot(snapshotInfo, snapshotInfo, commandCallback); + + verify(storageStrategy).revertSnapshotForCloudStackVolume( + eq("Legacy_UI_Snapshot"), eq("flexvol-uuid-1"), eq("clone-lun-uuid-2"), + eq("dest-lun-1"), eq("flexvol1")); + verify(commandCallback).complete(any(CommandResult.class)); + } + } + + @Test + void testDeleteAsync_SnapshotNfsClone_UsesDeleteFile() { + when(snapshotInfo.getType()).thenReturn(SNAPSHOT); + when(snapshotInfo.getId()).thenReturn(700L); + + when(snapshotDetailsDao.findDetail(700L, OntapStorageConstants.BASE_ONTAP_FV_ID)) + .thenReturn(new SnapshotDetailsVO(700L, OntapStorageConstants.BASE_ONTAP_FV_ID, "flexvol-uuid-nfs", false)); + when(snapshotDetailsDao.findDetail(700L, OntapStorageConstants.ONTAP_CLONE_ID)) + .thenReturn(new SnapshotDetailsVO(700L, OntapStorageConstants.ONTAP_CLONE_ID, "clone-id-nfs", false)); + when(snapshotDetailsDao.findDetail(700L, OntapStorageConstants.ONTAP_CLONE_NAME)) + .thenReturn(new SnapshotDetailsVO(700L, OntapStorageConstants.ONTAP_CLONE_NAME, "clone-file-nfs.qcow2", false)); + when(snapshotDetailsDao.findDetail(700L, OntapStorageConstants.PRIMARY_POOL_ID)) + .thenReturn(new SnapshotDetailsVO(700L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false)); + when(snapshotDetailsDao.findDetail(700L, OntapStorageConstants.PROTOCOL)) + .thenReturn(new SnapshotDetailsVO(700L, OntapStorageConstants.PROTOCOL, ProtocolType.NFS3.name(), false)); + + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + doNothing().when(storageStrategy).deleteSnapshotClone("flexvol-uuid-nfs", null, "clone-file-nfs.qcow2", "clone-id-nfs"); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + + driver.deleteAsync(dataStore, snapshotInfo, commandCallback); + + verify(storageStrategy).deleteSnapshotClone("flexvol-uuid-nfs", null, "clone-file-nfs.qcow2", "clone-id-nfs"); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class); + verify(commandCallback).complete(resultCaptor.capture()); + assertTrue(resultCaptor.getValue().isSuccess()); + } + } + + @Test + void testDeleteAsync_SnapshotIscsiClone_ResolvesUuidAndUsesDeleteLun() { + storagePoolDetails.put(OntapStorageConstants.VOLUME_NAME, "flexvol1"); + storagePoolDetails.put(OntapStorageConstants.SVM_NAME, "svm1"); + + when(snapshotInfo.getType()).thenReturn(SNAPSHOT); + when(snapshotInfo.getId()).thenReturn(701L); + + when(snapshotDetailsDao.findDetail(701L, OntapStorageConstants.BASE_ONTAP_FV_ID)) + .thenReturn(new SnapshotDetailsVO(701L, OntapStorageConstants.BASE_ONTAP_FV_ID, "flexvol-uuid-iscsi", false)); + when(snapshotDetailsDao.findDetail(701L, OntapStorageConstants.ONTAP_CLONE_ID)).thenReturn(null); + when(snapshotDetailsDao.findDetail(701L, OntapStorageConstants.ONTAP_CLONE_NAME)) + .thenReturn(new SnapshotDetailsVO(701L, OntapStorageConstants.ONTAP_CLONE_NAME, "clone-lun-name", false)); + when(snapshotDetailsDao.findDetail(701L, OntapStorageConstants.PRIMARY_POOL_ID)) + .thenReturn(new SnapshotDetailsVO(701L, OntapStorageConstants.PRIMARY_POOL_ID, "1", false)); + when(snapshotDetailsDao.findDetail(701L, OntapStorageConstants.PROTOCOL)) + .thenReturn(new SnapshotDetailsVO(701L, OntapStorageConstants.PROTOCOL, ProtocolType.ISCSI.name(), false)); + + when(storagePoolDetailsDao.listDetailsKeyPairs(1L)).thenReturn(storagePoolDetails); + doNothing().when(storageStrategy).deleteSnapshotClone("flexvol-uuid-iscsi", "flexvol1", "clone-lun-name", null); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + driver.deleteAsync(dataStore, snapshotInfo, commandCallback); + + verify(storageStrategy).deleteSnapshotClone("flexvol-uuid-iscsi", "flexvol1", "clone-lun-name", null); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(CommandResult.class); + verify(commandCallback).complete(resultCaptor.capture()); + assertTrue(resultCaptor.getValue().isSuccess()); + } + } } 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 86ef1d7c79b6..fb80101a8850 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 @@ -144,10 +144,18 @@ public CloudStackVolume getCloudStackVolume(Map cloudStackVolume } @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, String lunUuid, String flexVolName) { + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, String flexVolName) { + } + + @Override + public String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, String cloneName, String sourceObjectUuid) { return null; } + @Override + public void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, String cloneObjectUuid) { + } + @Override public AccessGroup createAccessGroup(AccessGroup accessGroup) { return null; 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 c4d5ddf6878c..3baa5c284261 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 @@ -75,6 +75,7 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.argThat; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -137,6 +138,9 @@ private void injectField(String fieldName, Object mockedField) throws Exception } private class TestableUnifiedNASStrategy extends UnifiedNASStrategy { + private boolean jobPollResult = true; + private String lastPolledJobUuid; + public TestableUnifiedNASStrategy(OntapStorage ontapStorage, NASFeignClient nasFeignClient, VolumeFeignClient volumeFeignClient, @@ -165,6 +169,12 @@ private void injectParentMockedClient(String fieldName, Object mockedClient) { throw new RuntimeException("Failed to inject parent mocked client: " + fieldName, e); } } + + @Override + public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeInMilliSecs) { + this.lastPolledJobUuid = jobUUID; + return jobPollResult; + } } // Test createCloudStackVolume - Success @@ -582,4 +592,30 @@ public void testDeleteCloudStackVolume_AnswerNull() throws Exception { strategy.deleteCloudStackVolume(cloudStackVolume); }); } + + @Test + public void testRevertSnapshotForCloudStackVolume_UsesCloneFileRestore() { + JobResponse cloneJobResponse = new JobResponse(); + Job job = new Job(); + job.setUuid("job-uuid-1"); + cloneJobResponse.setJob(job); + when(nasFeignClient.cloneFile(anyString(), any())).thenReturn(cloneJobResponse); + + strategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "snap-uuid-1", "vm-disk.qcow2", "flexvol1"); + verify(nasFeignClient).cloneFile(anyString(), argThat(req -> + req != null + && "clone-snap-1".equals(req.getSourcePath()) + && "vm-disk.qcow2".equals(req.getDestinationPath()) + && req.getVolume() != null + && "flexvol-uuid-1".equals(req.getVolume().getUuid()) + && "flexvol1".equals(req.getVolume().getName()))); + assertEquals("job-uuid-1", strategy.lastPolledJobUuid); + } + + @Test + public void testRevertSnapshotForCloudStackVolume_MissingFlexVolUuid_Throws() { + assertThrows(CloudRuntimeException.class, () -> strategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", null, "snap-uuid-1", "vm-disk.qcow2", "flexvol1")); + } } 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 1c0c84ef91dd..9b886b7e2965 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 @@ -59,6 +59,7 @@ 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; @@ -1802,4 +1803,43 @@ void testEnsureLunMapped_ExistingMapping_ReturnsExistingNumber() { verify(sanFeignClient, never()).createLunMap(any(), anyBoolean(), any(LunMap.class)); } } + + @Test + void testRevertSnapshotForCloudStackVolume_UsesLunPatchWithCloneSource() { + OntapResponse destinationLunResponse = new OntapResponse<>(); + Lun destinationLun = new Lun(); + destinationLun.setUuid("dest-lun-uuid-1"); + destinationLunResponse.setRecords(List.of(destinationLun)); + when(sanFeignClient.getLunResponse(eq(authHeader), anyMap())).thenReturn(destinationLunResponse); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password")) + .thenReturn(authHeader); + utilityMock.when(() -> OntapStorageUtils.getLunName("flexvol1", "clone-snap-1")) + .thenReturn("/vol/flexvol1/clone-snap-1"); + utilityMock.when(() -> OntapStorageUtils.getLunName("flexvol1", "dest-lun-1")) + .thenReturn("/vol/flexvol1/dest-lun-1"); + + unifiedSANStrategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "clone-lun-uuid-1", "dest-lun-1", "flexvol1"); + + verify(sanFeignClient).updateLun(eq(authHeader), eq("dest-lun-uuid-1"), argThat(lun -> + lun != null + && lun.getIsOverride() == null + && lun.getName() == null + && lun.getClone() != null + && lun.getClone().getSource() != null + && "/vol/flexvol1/clone-snap-1".equals(lun.getClone().getSource().getName()) + && "clone-lun-uuid-1".equals(lun.getClone().getSource().getUuid()) + && lun.getLocation() == null + && lun.getSvm() == null + )); + } + } + + @Test + void testRevertSnapshotForCloudStackVolume_MissingSnapshotUuid_Throws() { + assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", null, "dest-lun-1", "flexvol1")); + } } diff --git a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java index b069ab7246a0..ff88d0740f97 100644 --- a/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java +++ b/plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java @@ -337,20 +337,16 @@ void testCanHandle_NonAllocated_HasFlexVolSnapshotDetails_AllOnOntap_ReturnsHigh } @Test - void testCanHandle_NonAllocated_HasLegacyStorageSnapshotDetails_AllOnOntap_ReturnsHighest() { + void testCanHandle_NonAllocated_HasLegacyStorageSnapshotDetails_AllOnOntap_ReturnsCantHandle() { setupAllVolumesOnOntap(); VMSnapshotVO vmSnapshot = createMockVmSnapshot(VMSnapshot.State.Ready, VMSnapshot.Type.Disk); - // No FlexVol details + // Only clone-backed ONTAP details are supported now. when(vmSnapshotDetailsDao.findDetails(SNAPSHOT_ID, OntapStorageConstants.ONTAP_FLEXVOL_SNAPSHOT)).thenReturn(Collections.emptyList()); - // Has legacy details - List details = new ArrayList<>(); - details.add(new VMSnapshotDetailsVO(SNAPSHOT_ID, "kvmStorageSnapshot", "123", true)); - when(vmSnapshotDetailsDao.findDetails(SNAPSHOT_ID, "kvmStorageSnapshot")).thenReturn(details); StrategyPriority result = strategy.canHandle(vmSnapshot); - assertEquals(StrategyPriority.HIGHEST, result); + assertEquals(StrategyPriority.CANT_HANDLE, result); } @Test @@ -539,8 +535,8 @@ void testGroupVolumesByFlexVol_VolumeNotFound_ThrowsException() { @Test void testFlexVolSnapshotDetail_ParseAndToString_NewFormat() { String value = "flexvol-uuid-1::snap-uuid-1::vmsnap_200_1234567890::root-disk.qcow2::401::NFS3"; - OntapVMSnapshotStrategy.FlexVolSnapshotDetail detail = - OntapVMSnapshotStrategy.FlexVolSnapshotDetail.parse(value); + OntapVMSnapshotStrategy.CSVolSnapshotDetail detail = + OntapVMSnapshotStrategy.CSVolSnapshotDetail.parse(value); assertEquals("flexvol-uuid-1", detail.flexVolUuid); assertEquals("snap-uuid-1", detail.snapshotUuid); @@ -555,8 +551,8 @@ void testFlexVolSnapshotDetail_ParseAndToString_NewFormat() { void testFlexVolSnapshotDetail_ParseLegacy4FieldFormat() { // Legacy format without volumePath and protocol String value = "flexvol-uuid-1::snap-uuid-1::vmsnap_200_1234567890::401"; - OntapVMSnapshotStrategy.FlexVolSnapshotDetail detail = - OntapVMSnapshotStrategy.FlexVolSnapshotDetail.parse(value); + OntapVMSnapshotStrategy.CSVolSnapshotDetail detail = + OntapVMSnapshotStrategy.CSVolSnapshotDetail.parse(value); assertEquals("flexvol-uuid-1", detail.flexVolUuid); assertEquals("snap-uuid-1", detail.snapshotUuid); @@ -569,20 +565,20 @@ void testFlexVolSnapshotDetail_ParseLegacy4FieldFormat() { @Test void testFlexVolSnapshotDetail_ParseInvalidFormat_ThrowsException() { assertThrows(CloudRuntimeException.class, - () -> OntapVMSnapshotStrategy.FlexVolSnapshotDetail.parse("invalid-format")); + () -> OntapVMSnapshotStrategy.CSVolSnapshotDetail.parse("invalid-format")); } @Test void testFlexVolSnapshotDetail_ParseTooFewParts_ThrowsException() { assertThrows(CloudRuntimeException.class, - () -> OntapVMSnapshotStrategy.FlexVolSnapshotDetail.parse("a::b::c")); + () -> OntapVMSnapshotStrategy.CSVolSnapshotDetail.parse("a::b::c")); } @Test void testFlexVolSnapshotDetail_Parse5Parts_ThrowsException() { // 5 parts is neither legacy (4) nor current (6) format assertThrows(CloudRuntimeException.class, - () -> OntapVMSnapshotStrategy.FlexVolSnapshotDetail.parse("a::b::c::d::e")); + () -> OntapVMSnapshotStrategy.CSVolSnapshotDetail.parse("a::b::c::d::e")); } // ══════════════════════════════════════════════════════════════════════════ @@ -592,11 +588,11 @@ void testFlexVolSnapshotDetail_Parse5Parts_ThrowsException() { @Test void testBuildSnapshotName_Format() { VMSnapshotVO vmSnapshot = mock(VMSnapshotVO.class); - when(vmSnapshot.getId()).thenReturn(SNAPSHOT_ID); + when(vmSnapshot.getName()).thenReturn("My VM Snapshot #1"); String name = strategy.buildSnapshotName(vmSnapshot); - assertEquals(true, name.startsWith("vmsnap_200_")); + assertEquals(true, name.startsWith("My_VM_Snapshot")); assertEquals(true, name.length() <= OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH); }