diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java index 04996b74d2a5..fdc21fd7d00f 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java @@ -54,11 +54,7 @@ import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao; import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient; -import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot; import org.apache.cloudstack.storage.feign.model.Lun; -import org.apache.cloudstack.storage.feign.model.response.JobResponse; -import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import org.apache.cloudstack.storage.service.SANStrategy; import org.apache.cloudstack.storage.service.StorageStrategy; import org.apache.cloudstack.storage.service.UnifiedSANStrategy; @@ -67,7 +63,6 @@ import org.apache.cloudstack.storage.service.model.ProtocolType; import org.apache.cloudstack.storage.to.SnapshotObjectTO; import org.apache.cloudstack.storage.utils.OntapStorageUtils; -import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; @@ -211,7 +206,7 @@ private CloudStackVolume createCloudStackVolume(StoragePoolVO storagePool, Volum * Deletes a volume or snapshot from the ONTAP storage system. * *
For volumes, deletes the backend storage object (LUN for iSCSI, no-op for NFS). - * For snapshots, deletes the FlexVolume snapshot from ONTAP that was created by takeSnapshot.
+ * For snapshots, deletes the clone artifact from ONTAP that was created by takeSnapshot. */ @Override public void deleteAsync(DataStore store, DataObject data, AsyncCompletionCallbackRetrieves the snapshot details stored during takeSnapshot and calls the ONTAP - * REST API to delete the FlexVolume snapshot.
- * - * @param snapshotInfo The CloudStack snapshot to delete - * @param commandResult Result object to populate with success/failure + * Deletes a clone-backed ONTAP snapshot object (NFS file clone or iSCSI LUN clone). */ - private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult commandResult) { + private void deleteCloneBackedSnapshot(SnapshotInfo snapshotInfo, CommandResult commandResult) { long snapshotId = snapshotInfo.getId(); - logger.info("deleteOntapSnapshot: Deleting ONTAP FlexVolume snapshot for CloudStack snapshot [{}]", snapshotId); + logger.info("deleteCloneBackedSnapshot: Deleting clone-backed ONTAP object for CloudStack snapshot [{}]", snapshotId); try { - // Retrieve snapshot details stored during takeSnapshot String flexVolUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.BASE_ONTAP_FV_ID); - String ontapSnapshotUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_ID); - String snapshotName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); + String cloneUuid = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_ID); + String cloneName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_NAME); String poolIdStr = getSnapshotDetail(snapshotId, OntapStorageConstants.PRIMARY_POOL_ID); + String protocol = getSnapshotDetail(snapshotId, OntapStorageConstants.PROTOCOL); - if (flexVolUuid == null || ontapSnapshotUuid == null) { - logger.warn("deleteOntapSnapshot: Missing ONTAP snapshot details for snapshot [{}]. " + - "flexVolUuid={}, ontapSnapshotUuid={}. Snapshot may have been created by a different method or already deleted.", - snapshotId, flexVolUuid, ontapSnapshotUuid); - // Consider this a success since there's nothing to delete on ONTAP + if (poolIdStr == null || protocol == null || cloneName == null) { + logger.warn("deleteCloneBackedSnapshot: Missing clone metadata for snapshot [{}]. " + + "poolId={}, protocol={}, cloneName={}. Treating as success.", + snapshotId, poolIdStr, protocol, cloneName); commandResult.setSuccess(true); commandResult.setResult(null); return; @@ -285,26 +273,17 @@ private void deleteOntapSnapshot(SnapshotInfo snapshotInfo, CommandResult comman MapThis method creates a point-in-time, space-efficient snapshot of the entire - * FlexVolume containing the CloudStack volume. FlexVolume snapshots are atomic - * and capture all files/LUNs within the volume at the moment of creation.
- * - *Both NFS and iSCSI protocols use the same FlexVolume snapshot approach: + *
Both NFS and iSCSI protocols use clone-backed snapshot semantics: *
This method delegates to the appropriate StorageStrategy to restore the - * specific file (NFS) or LUN (iSCSI) from the FlexVolume snapshot directly - * via ONTAP REST API, without involving the hypervisor agent.
+ * specific file (NFS) or LUN (iSCSI) from clone artifacts directly via ONTAP + * REST API, without involving the hypervisor agent. * *Protocol-specific handling (delegated to strategy classes):
*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}"}) - OntapResponseONTAP REST: {@code GET /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}
- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot - * @return The FlexVolSnapshot object - */ - @RequestLine("GET /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}") - @Headers({"Authorization: {authHeader}"}) - FlexVolSnapshot getSnapshotByUuid(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Deletes a specific snapshot. - * - *ONTAP REST: {@code DELETE /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}
- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot to delete - * @return JobResponse containing the async job reference - */ - @RequestLine("DELETE /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}") - @Headers({"Authorization: {authHeader}"}) - JobResponse deleteSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Restores a volume to a specific snapshot. - * - *ONTAP REST: {@code PATCH /api/storage/volumes/{volume_uuid}/snapshots/{uuid}} - * with body {@code {"restore": true}} triggers a snapshot restore operation.
- * - *Note: This is a destructive operation — all data written after the - * snapshot was taken will be lost.
- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot to restore to - * @param body Request body, typically {@code {"restore": true}} - * @return JobResponse containing the async job reference - */ - @RequestLine("PATCH /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}?restore_to_snapshot=true") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid); - - /** - * Restores a single file or LUN from a FlexVolume snapshot. - * - *ONTAP REST: - * {@code POST /api/storage/volumes/{volume_uuid}/snapshots/{snapshot_uuid}/files/{file_path}/restore}
- * - *This restores only the specified file/LUN from the snapshot to the - * given {@code destination_path}, without reverting the entire FlexVolume. - * Ideal when multiple VMs share the same FlexVolume.
- * - * @param authHeader Basic auth header - * @param volumeUuid UUID of the ONTAP FlexVolume - * @param snapshotUuid UUID of the snapshot containing the file - * @param filePath path of the file within the snapshot (URL-encoded if needed) - * @param request request body with {@code destination_path} - * @return JobResponse containing the async job reference - */ - @RequestLine("POST /api/storage/volumes/{volumeUuid}/snapshots/{snapshotUuid}/files/{filePath}/restore") - @Headers({"Authorization: {authHeader}", "Content-Type: application/json"}) - JobResponse restoreFileFromSnapshot(@Param("authHeader") String authHeader, - @Param("volumeUuid") String volumeUuid, - @Param("snapshotUuid") String snapshotUuid, - @Param("filePath") String filePath, - SnapshotFileRestoreRequest request); - - /** - * Restores a single file or LUN from a FlexVolume snapshot using the CLI native API. - * - *ONTAP REST (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}
- * - *This CLI-based API is more reliable and works for both NFS files and iSCSI LUNs. - * The request body contains all required parameters: vserver, volume, snapshot, and path.
- * - *Example payload: - *
- * {
- * "vserver": "vs0",
- * "volume": "rajiv_ONTAP_SP1",
- * "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
- * "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
- * }
- *
- *
- *
- * @param authHeader Basic auth header
- * @param request CLI snapshot restore request containing vserver, volume, snapshot, and path
- * @return JobResponse containing the async job reference (if applicable)
- */
- @RequestLine("POST /api/private/cli/volume/snapshot/restore-file")
- @Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
- JobResponse restoreFileFromSnapshotCli(@Param("authHeader") String authHeader,
- CliSnapshotRestoreRequest request);
-}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java
deleted file mode 100644
index be242523f534..000000000000
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/CliSnapshotRestoreRequest.java
+++ /dev/null
@@ -1,121 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-package org.apache.cloudstack.storage.feign.model;
-
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
-import com.fasterxml.jackson.annotation.JsonInclude;
-import com.fasterxml.jackson.annotation.JsonProperty;
-
-/**
- * Request body for the ONTAP CLI-based Snapshot File Restore API.
- *
- * ONTAP REST endpoint (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}
- * - *This API restores a single file or LUN from a FlexVolume snapshot to a - * specified destination path using the CLI native implementation. - * It works for both NFS files and iSCSI LUNs.
- * - *Example payload: - *
- * {
- * "vserver": "vs0",
- * "volume": "rajiv_ONTAP_SP1",
- * "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
- * "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
- * }
- *
- *
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class CliSnapshotRestoreRequest {
-
- @JsonProperty("vserver")
- private String vserver;
-
- @JsonProperty("volume")
- private String volume;
-
- @JsonProperty("snapshot")
- private String snapshot;
-
- @JsonProperty("path")
- private String path;
-
- public CliSnapshotRestoreRequest() {
- }
-
- /**
- * Creates a CLI snapshot restore request.
- *
- * @param vserver The SVM (vserver) name
- * @param volume The FlexVolume name
- * @param snapshot The snapshot name
- * @param path The file/LUN path to restore (e.g., "/uuid.qcow2" or "/lun_name")
- */
- public CliSnapshotRestoreRequest(String vserver, String volume, String snapshot, String path) {
- this.vserver = vserver;
- this.volume = volume;
- this.snapshot = snapshot;
- this.path = path;
- }
-
- public String getVserver() {
- return vserver;
- }
-
- public void setVserver(String vserver) {
- this.vserver = vserver;
- }
-
- public String getVolume() {
- return volume;
- }
-
- public void setVolume(String volume) {
- this.volume = volume;
- }
-
- public String getSnapshot() {
- return snapshot;
- }
-
- public void setSnapshot(String snapshot) {
- this.snapshot = snapshot;
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
-
- @Override
- public String toString() {
- return "CliSnapshotRestoreRequest{" +
- "vserver='" + vserver + '\'' +
- ", volume='" + volume + '\'' +
- ", snapshot='" + snapshot + '\'' +
- ", path='" + path + '\'' +
- '}';
- }
-}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
similarity index 57%
rename from plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java
rename to plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
index 1f02e0c07470..d3df9bc64ce2 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/SnapshotFileRestoreRequest.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/FileCloneRequest.java
@@ -18,31 +18,34 @@
*/
package org.apache.cloudstack.storage.feign.model;
-import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
-/**
- * Request body for the ONTAP Snapshot File Restore API.
- *
- * ONTAP REST endpoint: - * {@code POST /api/storage/volumes/{volume.uuid}/snapshots/{snapshot.uuid}/files/{file.path}/restore}
- * - *This API restores a single file or LUN from a FlexVolume snapshot to a - * specified destination path, without reverting the entire FlexVolume.
- */ -@JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -public class SnapshotFileRestoreRequest { +public class FileCloneRequest { + @JsonProperty("volume") + private VolumeRef volume; + + @JsonProperty("source_path") + private String sourcePath; @JsonProperty("destination_path") private String destinationPath; - public SnapshotFileRestoreRequest() { + public VolumeRef getVolume() { + return volume; } - public SnapshotFileRestoreRequest(String destinationPath) { - this.destinationPath = destinationPath; + public void setVolume(VolumeRef volume) { + this.volume = volume; + } + + public String getSourcePath() { + return sourcePath; + } + + public void setSourcePath(String sourcePath) { + this.sourcePath = sourcePath; } public String getDestinationPath() { @@ -52,4 +55,29 @@ public String getDestinationPath() { public void setDestinationPath(String destinationPath) { this.destinationPath = destinationPath; } + + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class VolumeRef { + @JsonProperty("name") + private String name; + + @JsonProperty("uuid") + private String uuid; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java index 364790958c8a..23c8d30f2f60 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/Lun.java @@ -86,6 +86,12 @@ public static PropertyClassEnum fromValue(String value) { @JsonProperty("clone") private Clone clone = null; + @JsonProperty("location") + private Location location = null; + + @JsonProperty("is_override") + private Boolean isOverride = null; + /** * The operating system type of the LUN.<br/> Required in POST when creating a LUN that is not a clone of another. Disallowed in POST when creating a LUN clone. */ @@ -260,6 +266,22 @@ public void setClone(Clone clone) { this.clone = clone; } + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + this.location = location; + } + + public Boolean getIsOverride() { + return isOverride; + } + + public void setIsOverride(Boolean isOverride) { + this.isOverride = isOverride; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -287,11 +309,14 @@ public String toString() { sb.append(" enabled: ").append(toIndentedString(enabled)).append("\n"); sb.append(" lunMaps: ").append(toIndentedString(lunMaps)).append("\n"); sb.append(" name: ").append(toIndentedString(name)).append("\n"); + sb.append(" clone: ").append(toIndentedString(clone)).append("\n"); sb.append(" osType: ").append(toIndentedString(osType)).append("\n"); sb.append(" serialNumber: ").append(toIndentedString(serialNumber)).append("\n"); sb.append(" space: ").append(toIndentedString(space)).append("\n"); sb.append(" svm: ").append(toIndentedString(svm)).append("\n"); sb.append(" uuid: ").append(toIndentedString(uuid)).append("\n"); + sb.append(" location: ").append(toIndentedString(location)).append("\n"); + sb.append(" isOverride: ").append(toIndentedString(isOverride)).append("\n"); sb.append("}"); return sb.toString(); } @@ -317,6 +342,15 @@ public Source getSource() { public void setSource(Source source) { this.source = source; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Clone {\n"); + sb.append(" source: ").append(source).append("\n"); + sb.append("}"); + return sb.toString(); + } } public static class Source { @@ -337,5 +371,59 @@ public String getUuid() { public void setUuid(String uuid) { this.uuid = uuid; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Source {\n"); + sb.append(" name: ").append(name).append("\n"); + sb.append(" uuid: ").append(uuid).append("\n"); + sb.append("}"); + return sb.toString(); + } + } + + public static class Location { + @JsonProperty("volume") + private LocationVolume volume = null; + + public LocationVolume getVolume() { + return volume; + } + + public void setVolume(LocationVolume volume) { + this.volume = volume; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class Location {\n"); + sb.append(" volume: ").append(volume).append("\n"); + sb.append("}"); + return sb.toString(); + } + } + + public static class LocationVolume { + @JsonProperty("name") + private String name = null; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class LocationVolume {\n"); + sb.append(" name: ").append(name).append("\n"); + sb.append("}"); + return sb.toString(); + } } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java deleted file mode 100644 index c645e4a5a16f..000000000000 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/model/LunRestoreRequest.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.cloudstack.storage.feign.model; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; - -/** - * Request body for the ONTAP LUN Restore API. - * - *ONTAP REST endpoint: - * {@code POST /api/storage/luns/{lun.uuid}/restore}
- * - *This API restores a LUN from a FlexVolume snapshot to a specified - * destination path. Unlike file restore, this is LUN-specific.
- * - *Example payload: - *
- * {
- * "snapshot": {
- * "name": "snapshot_name"
- * },
- * "destination": {
- * "path": "/vol/volume_name/lun_name"
- * }
- * }
- *
- *
- */
-@JsonIgnoreProperties(ignoreUnknown = true)
-@JsonInclude(JsonInclude.Include.NON_NULL)
-public class LunRestoreRequest {
-
- @JsonProperty("snapshot")
- private SnapshotRef snapshot;
-
- @JsonProperty("destination")
- private Destination destination;
-
- public LunRestoreRequest() {
- }
-
- public LunRestoreRequest(String snapshotName, String destinationPath) {
- this.snapshot = new SnapshotRef(snapshotName);
- this.destination = new Destination(destinationPath);
- }
-
- public SnapshotRef getSnapshot() {
- return snapshot;
- }
-
- public void setSnapshot(SnapshotRef snapshot) {
- this.snapshot = snapshot;
- }
-
- public Destination getDestination() {
- return destination;
- }
-
- public void setDestination(Destination destination) {
- this.destination = destination;
- }
-
- /**
- * Nested class for snapshot reference.
- */
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public static class SnapshotRef {
-
- @JsonProperty("name")
- private String name;
-
- public SnapshotRef() {
- }
-
- public SnapshotRef(String name) {
- this.name = name;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
- }
-
- /**
- * Nested class for destination path.
- */
- @JsonIgnoreProperties(ignoreUnknown = true)
- @JsonInclude(JsonInclude.Include.NON_NULL)
- public static class Destination {
-
- @JsonProperty("path")
- private String path;
-
- public Destination() {
- }
-
- public Destination(String path) {
- this.path = path;
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
- }
-}
diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
index bd808a26d6f8..8abdaac55bb2 100644
--- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
+++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/StorageStrategy.java
@@ -27,7 +27,6 @@
import org.apache.cloudstack.storage.feign.client.NetworkFeignClient;
import org.apache.cloudstack.storage.feign.client.NASFeignClient;
import org.apache.cloudstack.storage.feign.client.SANFeignClient;
-import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
import org.apache.cloudstack.storage.feign.client.SvmFeignClient;
import org.apache.cloudstack.storage.feign.client.VolumeFeignClient;
import org.apache.cloudstack.storage.feign.model.Aggregate;
@@ -70,7 +69,6 @@ public abstract class StorageStrategy {
protected NetworkFeignClient networkFeignClient;
protected SANFeignClient sanFeignClient;
protected NASFeignClient nasFeignClient;
- protected SnapshotFeignClient snapshotFeignClient;
protected OntapStorage storage;
@@ -94,7 +92,6 @@ public StorageStrategy(OntapStorage ontapStorage) {
this.networkFeignClient = feignClientFactory.createClient(NetworkFeignClient.class, baseURL);
this.sanFeignClient = feignClientFactory.createClient(SANFeignClient.class, baseURL);
this.nasFeignClient = feignClientFactory.createClient(NASFeignClient.class, baseURL);
- this.snapshotFeignClient = feignClientFactory.createClient(SnapshotFeignClient.class, baseURL);
}
// Connect method to validate ONTAP cluster, credentials, protocol, and SVM
@@ -527,27 +524,51 @@ public String getNetworkInterface() {
abstract public CloudStackVolume getCloudStackVolume(MapThis method encapsulates the snapshot revert behavior based on protocol:
+ *This method encapsulates clone-based restore behavior based on protocol:
*Implementations own all protocol-specific ONTAP interactions including any + * required async job polling.
+ * + * @param flexVolUuid FlexVolume UUID that contains the source object + * @param flexVolName FlexVolume name for path construction when needed + * @param sourcePath source file/LUN path + * @param cloneName destination clone name/path token + * @param sourceObjectUuid source object UUID (required by SAN, ignored by NAS) + * @return clone artifact UUID (or stable clone identifier for NAS) + */ + public abstract String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid); + + /** + * Deletes a protocol-specific clone artifact created for snapshot workflows. + * + * @param flexVolUuid FlexVolume UUID containing the clone + * @param flexVolName FlexVolume name for path construction when needed + * @param cloneName clone name/path token + * @param cloneObjectUuid clone UUID (required by SAN, ignored by NAS) + */ + public abstract void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, + String cloneObjectUuid); /** @@ -613,15 +634,6 @@ public abstract JobResponse revertSnapshotForCloudStackVolume(String snapshotNam */ abstract public String getLogicalAccess(MapONTAP REST API (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}
- * - *This method uses the CLI native API which is more reliable and works - * consistently for both NFS files and iSCSI LUNs.
- * - * @param snapshotName The ONTAP FlexVolume snapshot name - * @param flexVolUuid The FlexVolume UUID (not used in CLI API, kept for interface consistency) - * @param snapshotUuid The ONTAP snapshot UUID (not used in CLI API, kept for interface consistency) - * @param volumePath The file path within the FlexVolume - * @param lunUuid Not used for NFS (null) - * @param flexVolName The FlexVolume name (required for CLI API) - * @return JobResponse for the async restore operation + *The source clone file is copied back into the destination live file path. + * If destination already exists, this method uses a temporary backup path to + * preserve rollback safety.
*/ @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, - String lunUuid, String flexVolName) { - logger.info("revertSnapshotForCloudStackVolume [NFS]: Restoring file [{}] from snapshot [{}] on FlexVol [{}]", + String flexVolName) { + logger.info("revertSnapshotForCloudStackVolume [NFS]: Reverting file [{}] using clone [{}] on FlexVol [{}]", volumePath, snapshotName, flexVolName); if (snapshotName == null || snapshotName.isEmpty()) { - throw new CloudRuntimeException("Snapshot name is required for NFS snapshot revert"); + throw new CloudRuntimeException("Clone name is required for NFS snapshot revert"); } if (volumePath == null || volumePath.isEmpty()) { throw new CloudRuntimeException("File path is required for NFS snapshot revert"); } - if (flexVolName == null || flexVolName.isEmpty()) { - throw new CloudRuntimeException("FlexVolume name is required for NFS snapshot revert"); + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot revert"); } String authHeader = getAuthHeader(); - String svmName = storage.getSvmName(); + FileCloneRequest cloneRequest = new FileCloneRequest(); + FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(flexVolName); + cloneRequest.setVolume(volumeRef); + cloneRequest.setSourcePath(snapshotName); + cloneRequest.setDestinationPath(volumePath); + + try { + logger.debug("revertSnapshotForCloudStackVolume [NFS]: clone file source={} destination={}", + snapshotName, volumePath); + JobResponse cloneJobResponse = getNasFeignClient().cloneFile(authHeader, cloneRequest); + if (cloneJobResponse == null || cloneJobResponse.getJob() == null || + cloneJobResponse.getJob().getUuid() == null || cloneJobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException(String.format( + "cloneFile did not return a valid job response for source [%s] and destination [%s]", + snapshotName, volumePath)); + } + String cloneJobUuid = cloneJobResponse.getJob().getUuid(); + Boolean jobSucceeded = jobPollForSuccess(cloneJobUuid, 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException(String.format( + "cloneFile job [%s] failed for source [%s] and destination [%s]", + cloneJobUuid, snapshotName, volumePath)); + } + } catch (FeignException cloneEx) { + if (!isFileAlreadyExistsConflict(cloneEx)) { + throw cloneEx; + } + + String backupPath = volumePath + ".pre_revert_" + System.currentTimeMillis(); + logger.info("revertSnapshotForCloudStackVolume [NFS]: Destination [{}] exists, using backup path [{}] for safe restore", + volumePath, backupPath); + + FileInfo renameToBackup = new FileInfo(); + renameToBackup.setPath(backupPath); + getNasFeignClient().updateFile(authHeader, flexVolUuid, volumePath, true, renameToBackup); + + try { + JobResponse cloneJobResponse = getNasFeignClient().cloneFile(authHeader, cloneRequest); + if (cloneJobResponse == null || cloneJobResponse.getJob() == null || + cloneJobResponse.getJob().getUuid() == null || cloneJobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException(String.format( + "cloneFile did not return a valid job response for source [%s] and destination [%s]", + snapshotName, volumePath)); + } + String cloneJobUuid = cloneJobResponse.getJob().getUuid(); + Boolean jobSucceeded = jobPollForSuccess(cloneJobUuid, 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException(String.format( + "cloneFile job [%s] failed for source [%s] and destination [%s]", + cloneJobUuid, snapshotName, volumePath)); + } + try { + getNasFeignClient().deleteFile(authHeader, flexVolUuid, backupPath); + } catch (Exception cleanupEx) { + logger.warn("revertSnapshotForCloudStackVolume [NFS]: Backup cleanup failed for [{}]: {}", + backupPath, cleanupEx.getMessage()); + } + } catch (Exception restoreEx) { + try { + FileInfo rollbackRename = new FileInfo(); + rollbackRename.setPath(volumePath); + getNasFeignClient().updateFile(authHeader, flexVolUuid, backupPath, true, rollbackRename); + } catch (Exception rollbackEx) { + logger.error("revertSnapshotForCloudStackVolume [NFS]: Failed to roll back backup rename [{}] -> [{}]: {}", + backupPath, volumePath, rollbackEx.getMessage(), rollbackEx); + } + throw restoreEx; + } + } + + } - // Prepare the file path for ONTAP CLI API (ensure it starts with "/") - String ontapFilePath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + @Override + public String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid) { + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot clone create"); + } + if (sourcePath == null || sourcePath.isEmpty()) { + throw new CloudRuntimeException("Source file path is required for NFS snapshot clone create"); + } + if (cloneName == null || cloneName.isEmpty()) { + throw new CloudRuntimeException("Clone file name is required for NFS snapshot clone create"); + } - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapFilePath); + FileCloneRequest fileCloneRequest = new FileCloneRequest(); + FileCloneRequest.VolumeRef volumeRef = new FileCloneRequest.VolumeRef(); + volumeRef.setUuid(flexVolUuid); + volumeRef.setName(flexVolName); + fileCloneRequest.setVolume(volumeRef); + fileCloneRequest.setSourcePath(sourcePath); + fileCloneRequest.setDestinationPath(cloneName); + + JobResponse jobResponse = getNasFeignClient().cloneFile(getAuthHeader(), fileCloneRequest); + if (jobResponse == null || jobResponse.getJob() == null || + jobResponse.getJob().getUuid() == null || jobResponse.getJob().getUuid().isEmpty()) { + throw new CloudRuntimeException("Failed to initiate NFS clone create for " + cloneName); + } + Boolean jobSucceeded = jobPollForSuccess(jobResponse.getJob().getUuid(), 30, 2000); + if (jobSucceeded == null || !jobSucceeded) { + throw new CloudRuntimeException("NFS clone create job failed for " + cloneName); + } + return cloneName; + } - logger.info("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - svmName, flexVolName, snapshotName, ontapFilePath); + @Override + public void deleteSnapshotClone(String flexVolUuid, String flexVolName, String cloneName, String cloneObjectUuid) { + if (flexVolUuid == null || flexVolUuid.isEmpty()) { + throw new CloudRuntimeException("FlexVolume UUID is required for NFS snapshot clone delete"); + } + if (cloneName == null || cloneName.isEmpty()) { + throw new CloudRuntimeException("Clone file name is required for NFS snapshot clone delete"); + } + getNasFeignClient().deleteFile(getAuthHeader(), flexVolUuid, cloneName); + } - return getSnapshotFeignClient().restoreFileFromSnapshotCli(authHeader, restoreRequest); + private boolean isFileAlreadyExistsConflict(FeignException e) { + if (e.status() == 409) { + return true; + } + String message = e.getMessage(); + if (message == null) { + return false; + } + String lower = message.toLowerCase(); + return lower.contains("already exists") + || lower.contains("entry exists") + || lower.contains("duplicate"); } } diff --git a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java index 5f1ac265fc50..ab9eb84826b9 100644 --- a/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java +++ b/plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/service/UnifiedSANStrategy.java @@ -29,8 +29,6 @@ import org.apache.cloudstack.storage.feign.model.OntapStorage; import org.apache.cloudstack.storage.feign.model.Lun; import org.apache.cloudstack.storage.feign.model.LunMap; -import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest; -import org.apache.cloudstack.storage.feign.model.response.JobResponse; import org.apache.cloudstack.storage.feign.model.response.OntapResponse; import org.apache.cloudstack.storage.service.model.AccessGroup; import org.apache.cloudstack.storage.service.model.CloudStackVolume; @@ -543,52 +541,143 @@ public String ensureLunMapped(String svmName, String lunName, String accessGroup return response; } /** - * Reverts a LUN to a snapshot using the ONTAP CLI-based snapshot file restore API. + * Reverts a destination LUN from source clone LUN data. * - *ONTAP REST API (CLI passthrough): - * {@code POST /api/private/cli/volume/snapshot/restore-file}
- * - *This method uses the CLI native API which is more reliable and works - * consistently for both NFS files and iSCSI LUNs.
- * - * @param snapshotName The ONTAP FlexVolume snapshot name - * @param flexVolUuid The FlexVolume UUID (not used in CLI API, kept for interface consistency) - * @param snapshotUuid The ONTAP snapshot UUID (not used in CLI API, kept for interface consistency) - * @param volumePath The LUN name (used to construct the path) - * @param lunUuid The LUN UUID (not used in CLI API, kept for interface consistency) - * @param flexVolName The FlexVolume name (required for CLI API) - * @return JobResponse for the async restore operation + *Uses LUN PATCH clone-source semantics to overwrite the destination LUN + * contents while retaining clone metadata and strategy-driven path resolution.
*/ @Override - public JobResponse revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, + public void revertSnapshotForCloudStackVolume(String snapshotName, String flexVolUuid, String snapshotUuid, String volumePath, - String lunUuid, String flexVolName) { - logger.trace("revertSnapshotForCloudStackVolume [iSCSI]: Restoring LUN [{}] from snapshot [{}] on FlexVol [{}]", + String flexVolName) { + logger.trace("revertSnapshotForCloudStackVolume [iSCSI]: Reverting LUN [{}] from clone [{}] on FlexVol [{}]", volumePath, snapshotName, flexVolName); if (snapshotName == null || snapshotName.isEmpty()) { - throw new CloudRuntimeException("Snapshot name is required for iSCSI snapshot revert"); + throw new CloudRuntimeException("Source clone LUN name is required for iSCSI snapshot revert"); + } + if (volumePath == null || volumePath.isEmpty()) { + throw new CloudRuntimeException("Destination LUN name is required for iSCSI snapshot revert"); } if (flexVolName == null || flexVolName.isEmpty()) { throw new CloudRuntimeException("FlexVolume name is required for iSCSI snapshot revert"); } - if (volumePath == null || volumePath.isEmpty()) { - throw new CloudRuntimeException("LUN path is required for iSCSI snapshot revert"); + if (snapshotUuid == null || snapshotUuid.isEmpty()) { + throw new CloudRuntimeException("Source clone LUN UUID is required for iSCSI snapshot revert"); + } + if (storage.getSvmName() == null || storage.getSvmName().isEmpty()) { + throw new CloudRuntimeException("SVM name is required for iSCSI snapshot revert"); } - String authHeader = getAuthHeader(); - String svmName = storage.getSvmName(); + String sourceLunPath = snapshotName.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? snapshotName : OntapStorageUtils.getLunName(flexVolName, snapshotName); + String destinationLunPath = volumePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? volumePath : OntapStorageUtils.getLunName(flexVolName, volumePath); - // Prepare the LUN path for ONTAP CLI API (ensure it starts with "/") - String ontapLunPath = volumePath.startsWith("/") ? volumePath : "/" + volumePath; + if (!sourceLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path for iSCSI snapshot revert: " + sourceLunPath); + } + if (!destinationLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid destination LUN path for iSCSI snapshot revert: " + destinationLunPath); + } - // Create CLI snapshot restore request - CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest( - svmName, flexVolName, snapshotName, ontapLunPath); + String authHeader = getAuthHeader(); + String destinationLunUuid = resolveLunUuidByName(authHeader, storage.getSvmName(), destinationLunPath); + Lun revertCloneRequest = new Lun(); + // PATCH /storage/luns/{uuid} rejects immutable destination attributes like svm.name. + // For restore, only provide clone source details and target the destination via UUID in URI. + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(sourceLunPath); + source.setUuid(snapshotUuid); + clone.setSource(source); + revertCloneRequest.setClone(clone); + + logger.debug("revertSnapshotForCloudStackVolume [iSCSI]: patch lun destinationUuid={} sourcePath={} sourceUuid={} destinationLun={}", + destinationLunUuid, sourceLunPath, snapshotUuid, destinationLunPath); + sanFeignClient.updateLun(authHeader, destinationLunUuid, revertCloneRequest); + } - logger.trace("revertSnapshotForCloudStackVolume: Calling CLI file restore API with vserver={}, volume={}, snapshot={}, path={}", - svmName, flexVolName, snapshotName, ontapLunPath); + @Override + public String createSnapshotClone(String flexVolUuid, String flexVolName, String sourcePath, + String cloneName, String sourceObjectUuid) { + if (sourceObjectUuid == null || sourceObjectUuid.isEmpty()) { + throw new CloudRuntimeException("Source LUN UUID is required for iSCSI snapshot clone create"); + } + if (sourcePath == null || sourcePath.isEmpty()) { + throw new CloudRuntimeException("Source LUN path is required for iSCSI snapshot clone create"); + } + if (flexVolName == null || flexVolName.isEmpty()) { + throw new CloudRuntimeException("FlexVolume name is required for iSCSI snapshot clone create"); + } + + String sourceLunPath = sourcePath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? sourcePath : OntapStorageUtils.getLunName(flexVolName, sourcePath); + String cloneLunPath = cloneName.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX) + ? cloneName : OntapStorageUtils.getLunName(flexVolName, cloneName); + + if (!sourceLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid source LUN path for iSCSI snapshot clone create: " + sourceLunPath); + } + if (!cloneLunPath.startsWith(OntapStorageConstants.VOLUME_PATH_PREFIX)) { + throw new CloudRuntimeException("Invalid clone LUN path for iSCSI snapshot clone create: " + cloneLunPath); + } + if (storage.getSvmName() == null || storage.getSvmName().isEmpty()) { + throw new CloudRuntimeException("SVM name is mandatory for iSCSI snapshot clone create"); + } + + Lun cloneRequest = new Lun(); + cloneRequest.setName(cloneLunPath); + Svm svm = new Svm(); + svm.setName(storage.getSvmName()); + cloneRequest.setSvm(svm); + Lun.Location location = new Lun.Location(); + Lun.LocationVolume locationVolume = new Lun.LocationVolume(); + locationVolume.setName(flexVolName); + location.setVolume(locationVolume); + cloneRequest.setLocation(location); + Lun.Clone clone = new Lun.Clone(); + Lun.Source source = new Lun.Source(); + source.setName(sourceLunPath); + source.setUuid(sourceObjectUuid); + clone.setSource(source); + cloneRequest.setClone(clone); + + OntapResponseThis strategy handles VM-level (instance) snapshots for VMs whose volumes - * reside on ONTAP managed primary storage. Instead of creating per-file clones - * (the old approach), it takes ONTAP FlexVolume-level snapshots via the - * ONTAP REST API ({@code POST /api/storage/volumes/{uuid}/snapshots}).
+ * reside on ONTAP managed primary storage by creating per-volume clone artifacts + * (file clones for NAS, LUN clones for SAN). * - *When multiple CloudStack disks (ROOT + DATA) reside on the same ONTAP - * FlexVolume, a single FlexVolume snapshot atomically captures all of them. - * This is both faster and more storage-efficient than per-file clones.
+ *Each CloudStack volume in the VM snapshot is represented by a dedicated ONTAP + * clone artifact. The workflow stores one detail row per CloudStack volume so + * revert/delete can operate precisely per volume.
* *Each FlexVolume snapshot is stored as a detail row with: + *
Each clone artifact is stored as a detail row with: *
Returns {@code StrategyPriority.HIGHEST} when:
@@ -115,7 +108,7 @@ public class OntapVMSnapshotStrategy extends StorageVMSnapshotStrategy { private static final Logger logger = LogManager.getLogger(OntapVMSnapshotStrategy.class); - /** Separator used in the vm_snapshot_details value to delimit FlexVol UUID, snapshot UUID, snapshot name, and pool ID. */ + /** Separator used in vm_snapshot_details values for clone metadata serialization. */ static final String DETAIL_SEPARATOR = "::"; @Inject @@ -139,20 +132,15 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) { // For existing (non-Allocated) snapshots, check if we created them if (!VMSnapshot.State.Allocated.equals(vmSnapshotVO.getState())) { - // Check for our FlexVolume snapshot details first - ListVolumes are grouped by their parent FlexVolume UUID (from storage pool details). - * For each unique FlexVolume, exactly one ONTAP snapshot is created via - * {@code POST /api/storage/volumes/{uuid}/snapshots}. This means if a VM has - * ROOT and DATA disks on the same FlexVolume, only one snapshot is created.
+ *Volumes are grouped by FlexVolume UUID for efficient metadata lookup, but a + * dedicated clone artifact is created per CloudStack volume.
* *Memory Snapshots Not Supported: This strategy only supports disk-only * (crash-consistent) snapshots. Memory snapshots (snapshotmemory=true) are rejected - * with a clear error message. This is because ONTAP FlexVolume snapshots capture disk + * with a clear error message. This is because ONTAP clone-backed snapshots capture disk * state only, and allowing mixed snapshot chains (ONTAP disk + libvirt memory) would * cause issues during revert operations.
* @@ -286,8 +272,8 @@ public VMSnapshot takeVMSnapshot(VMSnapshot vmSnapshot) { FreezeThawVMAnswer thawAnswer = null; long startFreeze = 0; - // Track which FlexVolume snapshots were created (for rollback) - ListSince there is one detail row per CloudStack volume, multiple rows may reference - * the same FlexVol + snapshot combination. This method deduplicates to delete each - * underlying ONTAP snapshot only once.
+ * the respective CS Vol + snapshot combination. */ - void deleteFlexVolSnapshots(ListInstead of restoring the entire FlexVolume to a snapshot (which would affect - * other VMs/files on the same FlexVol), this method restores only the individual - * files or LUNs belonging to this VM using the dedicated ONTAP CLI snapshot file - * restore API:
+ *Each persisted detail row represents one volume and points to the clone artifact + * created during VM snapshot creation. Revert copies from the clone artifact back to + * the original volume object.
* - *{@code POST /api/private/cli/volume/snapshot/restore-file}
- * - *For each persisted detail row (one per CloudStack volume):
*One row is persisted per CloudStack volume. Multiple volumes may share the same @@ -886,7 +822,7 @@ static class FlexVolGroupInfo { * *
Serialized format: {@code "