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..77dda31f93a7 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,9 +54,9 @@ 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.FileCloneRequest; import org.apache.cloudstack.storage.feign.model.Lun; +import org.apache.cloudstack.storage.feign.model.Svm; import org.apache.cloudstack.storage.feign.model.response.JobResponse; import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import org.apache.cloudstack.storage.service.SANStrategy; @@ -67,7 +67,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; @@ -237,7 +236,7 @@ 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 + // Delete the clone object (file/LUN) that was created by takeSnapshot deleteOntapSnapshot((SnapshotInfo) data, commandResult); } else { throw new CloudRuntimeException("Unsupported data object type: " + data.getType()); @@ -252,30 +251,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) { long snapshotId = snapshotInfo.getId(); - logger.info("deleteOntapSnapshot: Deleting ONTAP FlexVolume snapshot for CloudStack snapshot [{}]", snapshotId); + logger.info("deleteOntapSnapshot: Deleting clone-backed ONTAP snapshot 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("deleteOntapSnapshot: Missing clone metadata for snapshot [{}]. " + + "poolId={}, protocol={}, cloneName={}. Treating as success.", + snapshotId, poolIdStr, protocol, cloneName); commandResult.setSuccess(true); commandResult.setResult(null); return; @@ -285,26 +277,31 @@ 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 + "]"); + String svmName = poolDetails.get(OntapStorageConstants.SVM_NAME); + + if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) { + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + logger.warn("deleteOntapSnapshot: Missing FlexVol UUID for NFS clone delete on snapshot [{}]. Treating as success.", snapshotId); + commandResult.setSuccess(true); + commandResult.setResult(null); + return; } + logger.info("deleteOntapSnapshot: Deleting NFS clone file [{}] on FlexVol [{}]", cloneName, flexVolUuid); + storageStrategy.getNasFeignClient().deleteFile(authHeader, flexVolUuid, cloneName); + } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { + if (cloneUuid == null || cloneUuid.isEmpty()) { + String cloneLunPath = OntapStorageUtils.getLunName(poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName); + cloneUuid = resolveLunUuidByName(storageStrategy, authHeader, svmName, cloneLunPath); + } + logger.info("deleteOntapSnapshot: Deleting iSCSI clone LUN [{}] (uuid={})", cloneName, cloneUuid); + storageStrategy.getSanFeignClient().deleteLun(authHeader, cloneUuid, Map.of("allow_delete_while_mapped", "true")); + } else { + throw new CloudRuntimeException("Unsupported protocol for snapshot delete: " + protocol); } - logger.info("deleteOntapSnapshot: Successfully deleted ONTAP snapshot [{}] (uuid={}) for CloudStack snapshot [{}]", - snapshotName, ontapSnapshotUuid, snapshotId); + logger.info("deleteOntapSnapshot: Successfully deleted clone object [{}] for CloudStack snapshot [{}]", + cloneName, snapshotId); commandResult.setSuccess(true); commandResult.setResult(null); @@ -314,7 +311,7 @@ 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("deleteOntapSnapshot: Snapshot clone object for CloudStack snapshot [{}] not found, " + "may have been already deleted. Treating as success.", snapshotId); commandResult.setSuccess(true); commandResult.setResult(null); @@ -639,8 +636,15 @@ 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 authHeader = null; + String protocol = null; + String flexVolUuid = null; + String cloneName = null; + String cloneLunPath = null; + String svmName = null; try { VolumeInfo volumeInfo = snapshot.getBaseVolume(); @@ -657,82 +661,157 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId()); - String protocol = poolDetails.get(OntapStorageConstants.PROTOCOL); - String flexVolUuid = poolDetails.get(OntapStorageConstants.VOLUME_UUID); + protocol = poolDetails.get(OntapStorageConstants.PROTOCOL); + flexVolUuid = poolDetails.get(OntapStorageConstants.VOLUME_UUID); + svmName = poolDetails.get(OntapStorageConstants.SVM_NAME); 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); + authHeader = storageStrategy.getAuthHeader(); 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 cloneId = null; String lunUuid = null; - if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { + JobResponse nfsJobResponse = null; + + if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) { + FileCloneRequest fileCloneRequest = new FileCloneRequest(); + FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(poolDetails.get(OntapStorageConstants.VOLUME_NAME)); + fileCloneRequest.setVolume(volumeRef); + fileCloneRequest.setSourcePath(volumePath); + fileCloneRequest.setDestinationPath(cloneName); + logger.info("takeSnapshot: Creating NFS file clone [{}] from source [{}] on FlexVol UUID [{}]", + cloneName, volumePath, flexVolUuid); + nfsJobResponse = storageStrategy.getNasFeignClient().cloneFile(authHeader, fileCloneRequest); + cloneId = cloneName; + } else 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()); } + if (volumePath == null || volumePath.isEmpty()) { + throw new CloudRuntimeException("Source LUN path is missing for iSCSI volume " + volumeVO.getId()); + } + if (!volumePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path (must start with " + + OntapStorageConstants.VOLUME_PATH_PREFIX + "): " + volumePath); + } + cloneLunPath = OntapStorageUtils.getLunName( + poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName); + if (!cloneLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid iSCSI clone LUN path generated: " + cloneLunPath); + } + String svmNameForClone = poolDetails.get(OntapStorageConstants.SVM_NAME); + String flexVolNameForClone = poolDetails.get(OntapStorageConstants.VOLUME_NAME); + if (svmNameForClone == null || svmNameForClone.isEmpty()) { + throw new CloudRuntimeException("SVM name is mandatory for iSCSI clone request"); + } + if (flexVolNameForClone == null || flexVolNameForClone.isEmpty()) { + throw new CloudRuntimeException("FlexVolume name is mandatory for iSCSI clone request"); + } + Lun cloneRequest = new Lun(); + cloneRequest.setName(cloneLunPath); + Svm svm = new Svm(); + svm.setName(svmNameForClone); + cloneRequest.setSvm(svm); + Lun.Location location = new Lun.Location(); + Lun.LocationVolume locationVolume = new Lun.LocationVolume(); + locationVolume.setName(flexVolNameForClone); + location.setVolume(locationVolume); + cloneRequest.setLocation(location); + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(volumePath); + source.setUuid(lunUuid); + clone.setSource(source); + cloneRequest.setClone(clone); + logger.info("takeSnapshot: Creating iSCSI LUN clone [{}] from source LUN UUID [{}]", cloneName, lunUuid); + OntapResponse createCloneResponse = storageStrategy.getSanFeignClient().createLun(authHeader, true, cloneRequest); + if (createCloneResponse == null || createCloneResponse.getRecords() == null || createCloneResponse.getRecords().isEmpty()) { + throw new CloudRuntimeException("Failed to create iSCSI clone LUN for volume " + volumeVO.getId()); + } + cloneId = createCloneResponse.getRecords().get(0).getUuid(); + if (cloneId == null || cloneId.isEmpty()) { + cloneId = resolveLunUuidByName(storageStrategy, authHeader, svmNameForClone, cloneLunPath); + } + } else { + throw new CloudRuntimeException("Unsupported protocol for snapshot clone: " + protocol); } - // 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()); - - 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 + "]"); + if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) { + if (nfsJobResponse == null || nfsJobResponse.getJob() == null) { + throw new CloudRuntimeException("Failed to initiate clone-backed snapshot for volume " + volumeVO.getId()); + } + // Poll for async NFS clone completion + Boolean jobSucceeded = storageStrategy.jobPollForSuccess(nfsJobResponse.getJob().getUuid(), 30, 2000); + if (!jobSucceeded) { + throw new CloudRuntimeException("Clone create job failed for snapshot " + cloudStackSnapshotName); + } } - // 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, authHeader, protocol, flexVolUuid, + cloneName, cloneLunPath, svmName); + 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 authHeader, String protocol, + String flexVolUuid, String cloneName, String cloneLunPath, String svmName) { + if (storageStrategy == null || authHeader == null || protocol == null || cloneName == null || cloneName.isEmpty()) { + return "not-attempted"; + } + try { + if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) { + storageStrategy.getNasFeignClient().deleteFile(authHeader, flexVolUuid, cloneName); + return "nfs-clone-deleted"; + } + if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) { + String lunNameForLookup = cloneLunPath != null ? cloneLunPath : cloneName; + String cloneUuid = resolveLunUuidByName(storageStrategy, authHeader, svmName, lunNameForLookup); + storageStrategy.getSanFeignClient().deleteLun(authHeader, cloneUuid, Map.of("allow_delete_while_mapped", "true")); + return "iscsi-clone-deleted"; + } + return "unsupported-protocol"; + } catch (Exception cleanupEx) { + String cleanupMessage = cleanupEx.getMessage() != null ? cleanupEx.getMessage() : cleanupEx.toString(); + logger.warn("rollbackPartialSnapshotClone: Failed to clean up clone [{}] for protocol [{}]: {}", + cloneName, protocol, cleanupMessage); + return "cleanup-failed:" + cleanupMessage; + } + } + /** * Resolves the volume path on ONTAP for snapshot restore operations. * @@ -760,26 +839,13 @@ private String resolveVolumePathOnOntap(VolumeVO volumeVO, String protocol, Map< throw new CloudRuntimeException("Unsupported protocol " + protocol); } - /** - * 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(); + private String resolveLunUuidByName(StorageStrategy storageStrategy, String authHeader, String svmName, String lunName) { + OntapResponse lunResponse = storageStrategy.getSanFeignClient().getLunResponse(authHeader, + Map.of(OntapStorageConstants.SVM_DOT_NAME, svmName, OntapStorageConstants.NAME, lunName)); + if (lunResponse == null || lunResponse.getRecords() == null || lunResponse.getRecords().isEmpty()) { + throw new CloudRuntimeException("Failed to resolve LUN UUID for clone " + lunName); } - return null; + return lunResponse.getRecords().get(0).getUuid(); } /** @@ -814,15 +880,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 + ")"); } @@ -840,28 +910,21 @@ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snaps // 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); + lunUuid = ontapCloneId; } // Delegate to strategy class for protocol-specific restore - JobResponse jobResponse = storageStrategy.revertSnapshotForCloudStackVolume( - snapshotName, flexVolUuid, ontapSnapshotUuid, volumePath, lunUuid, flexVolName); + storageStrategy.revertSnapshotForCloudStackVolume( + ontapCloneName, flexVolUuid, ontapCloneId, volumePath, lunUuid, 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: iSCSI restore for [{}] completed without async job response; treating as synchronous success", volumePath); + + callback.complete(result); - 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,21 +1037,6 @@ 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. * @@ -1003,7 +1051,7 @@ private String buildSnapshotName(String volumeName, String snapshotUuid) { * @param lunUuid LUN UUID (only for iSCSI, null for NFS) */ private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String flexVolUuid, - String ontapSnapshotUuid, String snapshotName, + String ontapCloneId, String snapshotName, String ontapCloneName, String volumePath, long storagePoolId, String protocol, String lunUuid) { SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(csSnapshotId, @@ -1015,13 +1063,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); 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..f004cc0e5444 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 @@ -543,9 +540,9 @@ public String getNetworkInterface() { * @param volumePath The path of the file/LUN within the FlexVolume * @param lunUuid The LUN UUID (only for iSCSI, null for NFS) * @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); @@ -613,15 +610,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 +618,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..6e28cad4736d 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 @@ -42,7 +42,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; @@ -448,38 +447,36 @@ private FileInfo getFile(String volumeUuid, String filePath) { * @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 + * @return void */ @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 [{}]", + 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(); + // Keep PATCH-based revert. ONTAP in this environment rejects "target", so send + // only accepted fields and use "path" to carry source clone file reference. + FileInfo filePatchRequest = new FileInfo(); + filePatchRequest.setPath(snapshotName); + filePatchRequest.setOverwriteEnabled(Boolean.TRUE); + filePatchRequest.setFillEnabled(Boolean.FALSE); - // Prepare the file path for ONTAP CLI API (ensure it starts with "/") - String ontapFilePath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + logger.debug("revertSnapshotForCloudStackVolume [NFS]: patch file source={} destination={} overwrite=true fill=false", + snapshotName, volumePath); + getNasFeignClient().updateFile(authHeader, flexVolUuid, volumePath, true, filePatchRequest); - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapFilePath); - - logger.info("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - svmName, flexVolName, snapshotName, ontapFilePath); - - return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); } } 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..7650ccb4d691 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; @@ -557,38 +555,67 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup * @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 + * @return void */ @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 [{}]", + 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 (lunUuid == null || lunUuid.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(); - - // Prepare the LUN path for ONTAP CLI API (ensure it starts with "/") - String ontapLunPath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + 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); - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapLunPath); + 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); + } - logger.trace("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - 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(lunUuid); + clone.setSource(source); + revertCloneRequest.setClone(clone); + + logger.debug("revertSnapshotForCloudStackVolume [iSCSI]: patch lun destinationUuid={} sourcePath={} sourceUuid={} destinationLun={}", + destinationLunUuid, sourceLunPath, lunUuid, destinationLunPath); + sanFeignClient.updateLun(authHeader, destinationLunUuid, revertCloneRequest); + } - return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); + 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..f6b5f25a0e4f 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,11 +100,13 @@ 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"; 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..e18d12099d57 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,9 +32,7 @@ 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.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.StorageStrategy; @@ -148,11 +146,6 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { } 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; } @@ -350,8 +343,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 +366,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 +377,91 @@ 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); + String cloneName = buildPerVolumeCloneName(snapshotNameBase, vmSnapshot.getId(), volumeId); + String cloneUuid = cloneName; + if (ProtocolType.NFS3.name().equalsIgnoreCase(protocol)) { + org.apache.cloudstack.storage.feign.model.FileCloneRequest cloneRequest = new org.apache.cloudstack.storage.feign.model.FileCloneRequest(); + org.apache.cloudstack.storage.feign.model.FileCloneRequest.VolumeRef volumeRef = new org.apache.cloudstack.storage.feign.model.FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME)); + cloneRequest.setVolume(volumeRef); + cloneRequest.setSourcePath(volumePath); + cloneRequest.setDestinationPath(cloneName); + JobResponse fileJobResponse = storageStrategy.getNasFeignClient().cloneFile(authHeader, cloneRequest); + if (fileJobResponse == null || fileJobResponse.getJob() == null) { + throw new CloudRuntimeException("Failed to submit clone-backed VM snapshot for volume " + volumeId); + } + Boolean jobSucceeded = storageStrategy.jobPollForSuccess(fileJobResponse.getJob().getUuid(), 30, 2000); + if (!jobSucceeded) { + throw new CloudRuntimeException("Clone-backed VM snapshot job failed for volume " + volumeId); + } + } else 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); + } + if (volumePath == null || volumePath.isEmpty()) { + throw new CloudRuntimeException("Source LUN path is missing for volume " + volumeId); + } + if (!volumePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path (must start with " + + OntapStorageConstants.VOLUME_PATH_PREFIX + "): " + volumePath); + } + String cloneLunPath = OntapStorageUtils.getLunName( + groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName); + if (!cloneLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid iSCSI clone LUN path generated: " + cloneLunPath); + } + String svmName = groupInfo.poolDetails.get(OntapStorageConstants.SVM_NAME); + String flexVolName = groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME); + if (svmName == null || svmName.isEmpty()) { + throw new CloudRuntimeException("SVM name is mandatory for iSCSI clone request"); + } + if (flexVolName == null || flexVolName.isEmpty()) { + throw new CloudRuntimeException("FlexVolume name is mandatory for iSCSI clone request"); + } + org.apache.cloudstack.storage.feign.model.Lun cloneRequest = new org.apache.cloudstack.storage.feign.model.Lun(); + cloneRequest.setName(cloneLunPath); + org.apache.cloudstack.storage.feign.model.Svm svm = new org.apache.cloudstack.storage.feign.model.Svm(); + svm.setName(svmName); + cloneRequest.setSvm(svm); + org.apache.cloudstack.storage.feign.model.Lun.Location location = new org.apache.cloudstack.storage.feign.model.Lun.Location(); + org.apache.cloudstack.storage.feign.model.Lun.LocationVolume locationVolume = new org.apache.cloudstack.storage.feign.model.Lun.LocationVolume(); + locationVolume.setName(flexVolName); + location.setVolume(locationVolume); + cloneRequest.setLocation(location); + org.apache.cloudstack.storage.feign.model.Lun.Clone clone = new org.apache.cloudstack.storage.feign.model.Lun.Clone(); + org.apache.cloudstack.storage.feign.model.Lun.Source source = new org.apache.cloudstack.storage.feign.model.Lun.Source(); + source.setName(volumePath); + source.setUuid(sourceLunUuid); + clone.setSource(source); + cloneRequest.setClone(clone); + logger.info("CloneRequest: {}", cloneRequest); + OntapResponse createCloneResponse = storageStrategy.getSanFeignClient().createLun(authHeader, true, cloneRequest); + if (createCloneResponse == null || createCloneResponse.getRecords() == null || createCloneResponse.getRecords().isEmpty()) { + throw new CloudRuntimeException("Failed to create iSCSI clone LUN for volume " + volumeId); + } + cloneUuid = createCloneResponse.getRecords().get(0).getUuid(); + if (cloneUuid == null || cloneUuid.isEmpty()) { + cloneUuid = resolveLunUuid(storageStrategy, authHeader, svmName, cloneLunPath); + } + } else { + throw new CloudRuntimeException("Unsupported protocol for VM snapshot clone: " + protocol); + } FlexVolSnapshotDetail detail = new FlexVolSnapshotDetail( - flexVolUuid, snapshotUuid, snapshotNameBase, volumePath, groupInfo.poolId, protocol); + 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); } @@ -476,7 +515,11 @@ 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) Map rolledBack = new HashMap<>(); @@ -545,18 +588,12 @@ public boolean deleteVMSnapshot(VMSnapshot vmSnapshot) { DeleteVMSnapshotCommand deleteSnapshotCommand = new DeleteVMSnapshotCommand(vmInstanceName, vmSnapshotTO, volumeTOs, guestOS.getDisplayName()); - // Check for FlexVolume snapshots (new approach) + // Check for FlexVolume snapshots 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); - } - processAnswer(vmSnapshotVO, userVm, new DeleteVMSnapshotAnswer(deleteSnapshotCommand, volumeTOs), null); long fullChainSize = 0; for (VolumeObjectTO volumeTo : volumeTOs) { @@ -600,16 +637,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_FLEXVOL_SNAPSHOT); + if (CollectionUtils.isNotEmpty(cloneDetails)) { + revertCloneBackedSnapshots(cloneDetails); } RevertToVMSnapshotAnswer answer = new RevertToVMSnapshotAnswer(revertToSnapshotCommand, true, ""); @@ -672,25 +705,22 @@ 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); + String buildPerVolumeCloneName(String snapshotNameBase, Long vmSnapshotId, Long volumeId) { + return OntapStorageUtils.getOntapCloneName(snapshotNameBase + "_s" + vmSnapshotId + "_v" + volumeId); + } + + String resolveLunUuid(StorageStrategy strategy, String authHeader, String svmName, String lunName) { + OntapResponse response = strategy.getSanFeignClient() + .getLunResponse(authHeader, Map.of(OntapStorageConstants.SVM_DOT_NAME, svmName, OntapStorageConstants.NAME, lunName)); if (response == null || response.getRecords() == null || response.getRecords().isEmpty()) { - throw new CloudRuntimeException("Could not find FlexVolume snapshot [" + snapshotName + - "] on FlexVol [" + flexVolUuid + "] after creation"); + throw new CloudRuntimeException("Could not resolve LUN UUID for clone " + lunName); } return response.getRecords().get(0).getUuid(); } @@ -736,15 +766,22 @@ void rollbackFlexVolSnapshot(FlexVolSnapshotDetail detail) { try { 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); + if (ProtocolType.NFS3.name().equalsIgnoreCase(detail.protocol)) { + logger.info("rollbackFlexVolSnapshot: Deleting NFS clone file [{}] on FlexVol [{}]", + detail.snapshotName, detail.flexVolUuid); + storageStrategy.getNasFeignClient().deleteFile(authHeader, detail.flexVolUuid, detail.snapshotName); + } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(detail.protocol)) { + logger.info("rollbackFlexVolSnapshot: Deleting iSCSI clone LUN [{}] (uuid={})", + detail.snapshotName, detail.snapshotUuid); + String cloneUuid = detail.snapshotUuid; + if (cloneUuid == null || cloneUuid.isEmpty()) { + String svmName = poolDetails.get(OntapStorageConstants.SVM_NAME); + String cloneLunPath = OntapStorageUtils.getLunName(poolDetails.get(OntapStorageConstants.VOLUME_NAME), detail.snapshotName); + cloneUuid = resolveLunUuid(storageStrategy, authHeader, svmName, cloneLunPath); + } + storageStrategy.getSanFeignClient().deleteLun(authHeader, cloneUuid, Map.of("allow_delete_while_mapped", "true")); } } catch (Exception e) { logger.error("rollbackFlexVolSnapshot: Rollback of FlexVol snapshot failed: {}", e.getMessage(), e); @@ -770,19 +807,35 @@ void deleteFlexVolSnapshots(List flexVolDetails) { 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 { + if (ProtocolType.NFS3.name().equalsIgnoreCase(detail.protocol)) { + logger.info("deleteFlexVolSnapshots: Deleting NFS clone file [{}] on FlexVol [{}]", + detail.snapshotName, detail.flexVolUuid); + storageStrategy.getNasFeignClient().deleteFile(authHeader, detail.flexVolUuid, detail.snapshotName); + } else if (ProtocolType.ISCSI.name().equalsIgnoreCase(detail.protocol)) { + logger.info("deleteFlexVolSnapshots: Deleting iSCSI clone LUN [{}] (uuid={})", + detail.snapshotName, detail.snapshotUuid); + String cloneUuid = detail.snapshotUuid; + if (cloneUuid == null || cloneUuid.isEmpty()) { + String svmName = poolDetails.get(OntapStorageConstants.SVM_NAME); + String cloneLunPath = OntapStorageUtils.getLunName(poolDetails.get(OntapStorageConstants.VOLUME_NAME), detail.snapshotName); + cloneUuid = resolveLunUuid(storageStrategy, authHeader, svmName, cloneLunPath); + } + storageStrategy.getSanFeignClient().deleteLun(authHeader, cloneUuid, Map.of("allow_delete_while_mapped", "true")); + } + } catch (Exception e) { + if (isSnapshotAlreadyMissing(e)) { + logger.warn("deleteFlexVolSnapshots: 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("deleteFlexVolSnapshots: Deleted clone [{}] on FlexVol [{}]", detail.snapshotName, detail.flexVolUuid); } // Always remove the DB detail row @@ -790,72 +843,62 @@ 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. - * - *

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:

+ * Reverts all volumes of a VM snapshot using clone-backed restore operations. * - *

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

+ *

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.

* - *

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) { + void revertCloneBackedSnapshots(List cloneDetails) { + for (VMSnapshotDetailsVO detailVO : cloneDetails) { FlexVolSnapshotDetail detail = FlexVolSnapshotDetail.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); + String lunUuid = ProtocolType.ISCSI.name().equalsIgnoreCase(detail.protocol) ? detail.snapshotUuid : null; + try { + storageStrategy.revertSnapshotForCloudStackVolume( + detail.snapshotName, detail.flexVolUuid, detail.snapshotUuid, detail.volumePath, lunUuid, 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); } } 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..2d849296d5f6 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,33 @@ 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.Job; 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.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; @@ -54,8 +64,10 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static com.cloud.agent.api.to.DataObjectType.SNAPSHOT; import static com.cloud.agent.api.to.DataObjectType.VOLUME; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -71,6 +83,7 @@ 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 +102,9 @@ class OntapPrimaryDatastoreDriverTest { @Mock private VolumeDetailsDao volumeDetailsDao; + @Mock + private SnapshotDetailsDao snapshotDetailsDao; + @Mock private DataStore dataStore; @@ -107,6 +123,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 +592,195 @@ 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.getAuthHeader()).thenReturn("Basic auth"); + when(storageStrategy.getNasFeignClient()).thenReturn(nasFeignClient); + JobResponse jobResponse = new JobResponse(); + Job job = new Job(); + job.setUuid("job-uuid-1"); + jobResponse.setJob(job); + when(nasFeignClient.cloneFile(anyString(), any())).thenReturn(jobResponse); + when(storageStrategy.jobPollForSuccess("job-uuid-1", 30, 2000)).thenReturn(true); + + 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(nasFeignClient).cloneFile(anyString(), any()); + 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(), 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("clone-lun-uuid-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(), 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("clone-lun-uuid-2"), 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); + when(storageStrategy.getAuthHeader()).thenReturn("Basic auth"); + when(storageStrategy.getNasFeignClient()).thenReturn(nasFeignClient); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + + driver.deleteAsync(dataStore, snapshotInfo, commandCallback); + + verify(nasFeignClient).deleteFile("Basic auth", "flexvol-uuid-nfs", "clone-file-nfs.qcow2"); + 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); + when(storageStrategy.getAuthHeader()).thenReturn("Basic auth"); + when(storageStrategy.getSanFeignClient()).thenReturn(sanFeignClient); + + OntapResponse lunResponse = new OntapResponse<>(); + Lun lun = new Lun(); + lun.setUuid("resolved-clone-uuid"); + lunResponse.setRecords(List.of(lun)); + when(sanFeignClient.getLunResponse(eq("Basic auth"), any())).thenReturn(lunResponse); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.getStrategyByStoragePoolDetails(storagePoolDetails)) + .thenReturn(storageStrategy); + utilityMock.when(() -> OntapStorageUtils.getLunName("flexvol1", "clone-lun-name")) + .thenReturn("/vol/flexvol1/clone-lun-name"); + + driver.deleteAsync(dataStore, snapshotInfo, commandCallback); + + verify(sanFeignClient).deleteLun(eq("Basic auth"), eq("resolved-clone-uuid"), + argThat(map -> "true".equals(map.get("allow_delete_while_mapped")))); + 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..2c9060b46de2 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,8 +144,7 @@ public CloudStackVolume getCloudStackVolume(Map cloudStackVolume } @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, String lunUuid, String flexVolName) { - return null; + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, String lunUuid, String flexVolName) { } @Override 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..e41020a5ce6f 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) @@ -582,4 +583,22 @@ public void testDeleteCloudStackVolume_AnswerNull() throws Exception { strategy.deleteCloudStackVolume(cloudStackVolume); }); } + + @Test + public void testRevertSnapshotForCloudStackVolume_UsesFilePatchWithoutTarget() { + strategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "snap-uuid-1", "vm-disk.qcow2", null, "flexvol1"); + verify(nasFeignClient).updateFile(anyString(), eq("flexvol-uuid-1"), eq("vm-disk.qcow2"), eq(true), argThat(req -> + req != null + && Boolean.TRUE.equals(req.isOverwriteEnabled()) + && Boolean.FALSE.equals(req.isFillEnabled()) + && "clone-snap-1".equals(req.getPath()) + && req.getTarget() == null)); + } + + @Test + public void testRevertSnapshotForCloudStackVolume_MissingFlexVolUuid_Throws() { + assertThrows(CloudRuntimeException.class, () -> strategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", null, "snap-uuid-1", "vm-disk.qcow2", null, "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..f2df1865afad 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", "clone-lun-uuid-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_MissingLunUuid_Throws() { + assertThrows(CloudRuntimeException.class, () -> unifiedSANStrategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "clone-lun-uuid-1", "dest-lun-1", null, "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..60796ac0ff51 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 @@ -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); }