From 47f5aae0090a9f9ae84c7f8ddef158e6aea925ba Mon Sep 17 00:00:00 2001
From: "Jain, Rajiv" 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. 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. 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. 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"
- * }
- *
- *
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 51%
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..20b8d8d64faa 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,37 @@
*/
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() { + @JsonProperty("is_override") + private Boolean isOverride; + + 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 +58,37 @@ public String getDestinationPath() { public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; } + + public Boolean getIsOverride() { + return isOverride; + } + + public void setIsOverride(Boolean isOverride) { + this.isOverride = isOverride; + } + + @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..8f6c09acabb0 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) { @@ -338,4 +360,30 @@ public void setUuid(String uuid) { this.uuid = uuid; } } + + public static class Location { + @JsonProperty("volume") + private LocationVolume volume = null; + + public LocationVolume getVolume() { + return volume; + } + + public void setVolume(LocationVolume volume) { + this.volume = volume; + } + } + + public static class LocationVolume { + @JsonProperty("name") + private String name = null; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } } 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..9ade06eed3b7 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
@@ -630,6 +630,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..c26870d9f024 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
@@ -35,6 +35,7 @@
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
import org.apache.cloudstack.storage.feign.model.ExportRule;
import org.apache.cloudstack.storage.feign.model.FileInfo;
+import org.apache.cloudstack.storage.feign.model.FileCloneRequest;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
import org.apache.cloudstack.storage.feign.model.OntapStorage;
@@ -42,7 +43,6 @@
import org.apache.cloudstack.storage.feign.model.Volume;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
-import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.volume.VolumeObject;
@@ -454,32 +454,31 @@ private FileInfo getFile(String volumeUuid, String filePath) {
public JobResponse 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();
-
- // Prepare the file path for ONTAP CLI API (ensure it starts with "/")
- String ontapFilePath = volumePath.startsWith("/") ? volumePath : "/" + volumePath;
-
- // 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);
+ FileCloneRequest fileCloneRequest = new FileCloneRequest();
+ FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef();
+ volumeRef.setUuid(flexVolUuid);
+ volumeRef.setName(flexVolName);
+ fileCloneRequest.setVolume(volumeRef);
+ fileCloneRequest.setSourcePath(snapshotName);
+ fileCloneRequest.setDestinationPath(volumePath);
+ fileCloneRequest.setIsOverride(Boolean.TRUE);
+
+ logger.debug("revertSnapshotForCloudStackVolume [NFS]: file clone source={} destination={} isOverride=true",
+ snapshotName, volumePath);
+ return getNasFeignClient().cloneFile(authHeader, fileCloneRequest);
}
}
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..b80913c4759e 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,7 +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;
@@ -563,32 +562,39 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup
public JobResponse 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");
+ 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");
}
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;
-
- // Create CLI snapshot restore request
- CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest(
- svmName, flexVolName, snapshotName, ontapLunPath);
-
- logger.trace("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}",
- svmName, flexVolName, snapshotName, ontapLunPath);
-
- return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest);
+ Lun revertCloneRequest = new Lun();
+ revertCloneRequest.setName(volumePath);
+ Svm svm = new Svm();
+ svm.setName(storage.getSvmName());
+ revertCloneRequest.setSvm(svm);
+ Lun.Location location = new Lun.Location();
+ Lun.LocationVolume locationVolume = new Lun.LocationVolume();
+ locationVolume.setName(flexVolName);
+ location.setVolume(locationVolume);
+ revertCloneRequest.setLocation(location);
+ Lun.Clone clone = new Lun.Clone();
+ Lun.Source source = new Lun.Source();
+ source.setUuid(lunUuid);
+ clone.setSource(source);
+ revertCloneRequest.setClone(clone);
+ revertCloneRequest.setIsOverride(Boolean.TRUE);
+
+ logger.debug("revertSnapshotForCloudStackVolume [iSCSI]: lun clone sourceUuid={} destinationLun={} isOverride=true",
+ lunUuid, volumePath);
+ return sanFeignClient.cloneLun(authHeader, revertCloneRequest);
}
}
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..353f2a24aa8f 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
@@ -33,8 +33,6 @@
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
-import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
-import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot;
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
import org.apache.cloudstack.storage.service.StorageStrategy;
@@ -375,7 +373,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 +384,66 @@ 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 = snapshotNameBase;
+ String cloneUuid = cloneName;
+ JobResponse jobResponse;
+ 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);
+ cloneRequest.setIsOverride(Boolean.FALSE);
+ jobResponse = storageStrategy.getNasFeignClient().cloneFile(authHeader, cloneRequest);
+ } 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);
+ }
+ org.apache.cloudstack.storage.feign.model.Lun cloneRequest = new org.apache.cloudstack.storage.feign.model.Lun();
+ cloneRequest.setName(cloneName);
+ org.apache.cloudstack.storage.feign.model.Svm svm = new org.apache.cloudstack.storage.feign.model.Svm();
+ svm.setName(groupInfo.poolDetails.get(OntapStorageConstants.SVM_NAME));
+ 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(groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME));
+ 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.setUuid(sourceLunUuid);
+ clone.setSource(source);
+ cloneRequest.setClone(clone);
+ cloneRequest.setIsOverride(Boolean.FALSE);
+ jobResponse = storageStrategy.getSanFeignClient().cloneLun(authHeader, cloneRequest);
+ cloneUuid = resolveLunUuid(storageStrategy, authHeader, groupInfo.poolDetails.get(OntapStorageConstants.SVM_NAME), cloneName);
+ } else {
+ throw new CloudRuntimeException("Unsupported protocol for VM snapshot clone: " + protocol);
+ }
+ if (jobResponse == null || jobResponse.getJob() == null) {
+ throw new CloudRuntimeException("Failed to submit clone-backed VM snapshot for volume " + volumeId);
+ }
+ Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000);
+ if (!jobSucceeded) {
+ throw new CloudRuntimeException("Clone-backed VM snapshot job failed for volume " + volumeId);
+ }
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);
}
@@ -672,25 +693,14 @@ Map