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..f537b945e0b2 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 @@ -55,8 +55,9 @@ 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 +68,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; @@ -639,7 +639,7 @@ 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; try { @@ -665,64 +665,88 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback 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,16 +825,20 @@ 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 ontapCloneId = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_ID); String snapshotName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_SNAP_NAME); + String ontapCloneName = getSnapshotDetail(snapshotId, OntapStorageConstants.ONTAP_CLONE_NAME); + if (ontapCloneName == null) { + ontapCloneName = snapshotName; + } 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 || snapshotName == null || ontapCloneName == null || volumePath == null || poolIdStr == null) { throw new CloudRuntimeException("Missing required snapshot details for snapshot " + snapshotId + " (flexVolUuid=" + flexVolUuid + ", snapshotName=" + snapshotName + - ", volumePath=" + volumePath + ", poolId=" + poolIdStr + ")"); + ", cloneName=" + ontapCloneName + ", volumePath=" + volumePath + ", poolId=" + poolIdStr + ")"); } long poolId = Long.parseLong(poolIdStr); @@ -840,12 +855,12 @@ 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); + ontapCloneName, flexVolUuid, ontapCloneId, volumePath, lunUuid, flexVolName); if (jobResponse == null || jobResponse.getJob() == null) { throw new CloudRuntimeException("Failed to initiate restore from snapshot [" + @@ -974,21 +989,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 +1003,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 +1015,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..14d3011d81f8 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; @@ -58,6 +60,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..e9857a431acd 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); @@ -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 index 2f0e050d6f55..2f5a000caf8a 100644 --- 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 @@ -22,9 +22,7 @@ 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; @@ -107,78 +105,4 @@ 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 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..5bfcd582b235 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,69 @@ 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); + } + String cloneLunPath = OntapStorageUtils.getLunName( + groupInfo.poolDetails.get(OntapStorageConstants.VOLUME_NAME), cloneName); + 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(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), cloneLunPath); + } 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 +696,14 @@ 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. - */ - 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 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(); } @@ -819,43 +832,28 @@ void revertFlexVolSnapshots(List flexVolDetails) { 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); + detail.volumePath, detail.snapshotName, flexVolName, detail.protocol); + String lunUuid = ProtocolType.ISCSI.name().equalsIgnoreCase(detail.protocol) ? detail.snapshotUuid : null; + JobResponse jobResponse = storageStrategy.revertSnapshotForCloudStackVolume( + detail.snapshotName, detail.flexVolUuid, detail.snapshotUuid, detail.volumePath, lunUuid, flexVolName); 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 + + detail.volumePath + "] from snapshot [" + detail.snapshotName + "] on FlexVol [" + flexVolName + "]"); } } logger.info("revertFlexVolSnapshots: Successfully restored volume [{}] from snapshot [{}] on FlexVol [{}]", - ontapFilePath, detail.snapshotName, flexVolName); + 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..17c0896a2e9d 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,31 @@ 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.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.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; @@ -71,6 +79,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 +98,9 @@ class OntapPrimaryDatastoreDriverTest { @Mock private VolumeDetailsDao volumeDetailsDao; + @Mock + private SnapshotDetailsDao snapshotDetailsDao; + @Mock private DataStore dataStore; @@ -107,6 +119,15 @@ class OntapPrimaryDatastoreDriverTest { @Mock private UnifiedSANStrategy sanStrategy; + @Mock + private StorageStrategy storageStrategy; + + @Mock + private NASFeignClient nasFeignClient; + + @Mock + private SnapshotInfo snapshotInfo; + @Mock private AsyncCompletionCallback createCallback; @@ -564,4 +585,91 @@ 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_SNAP_NAME)) + .thenReturn(new SnapshotDetailsVO(500L, OntapStorageConstants.ONTAP_SNAP_NAME, "UI Snapshot Name", 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); + JobResponse jobResponse = new JobResponse(); + Job job = new Job(); + job.setUuid("job-uuid-2"); + jobResponse.setJob(job); + when(storageStrategy.revertSnapshotForCloudStackVolume(anyString(), anyString(), anyString(), anyString(), anyString(), anyString())) + .thenReturn(jobResponse); + when(storageStrategy.jobPollForSuccess("job-uuid-2", 60, 2000)).thenReturn(true); + + 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)); + } + } } 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..0e99ff68e942 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 @@ -37,6 +37,7 @@ import org.apache.cloudstack.storage.feign.client.NetworkFeignClient; import org.apache.cloudstack.storage.feign.client.SANFeignClient; import org.apache.cloudstack.storage.feign.model.ExportPolicy; +import org.apache.cloudstack.storage.feign.model.FileCloneRequest; import org.apache.cloudstack.storage.feign.model.Job; import org.apache.cloudstack.storage.feign.model.OntapStorage; import org.apache.cloudstack.storage.feign.model.response.JobResponse; @@ -75,6 +76,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 +584,33 @@ public void testDeleteCloudStackVolume_AnswerNull() throws Exception { strategy.deleteCloudStackVolume(cloudStackVolume); }); } + + @Test + public void testRevertSnapshotForCloudStackVolume_UsesFileCloneWithOverride() { + JobResponse jobResponse = new JobResponse(); + Job job = new Job(); + job.setUuid("job-uuid-1"); + jobResponse.setJob(job); + when(nasFeignClient.cloneFile(anyString(), any(FileCloneRequest.class))).thenReturn(jobResponse); + + JobResponse result = strategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "snap-uuid-1", "vm-disk.qcow2", null, "flexvol1"); + + assertNotNull(result); + verify(nasFeignClient).cloneFile(anyString(), argThat(req -> + req != null + && req.getIsOverride() != null + && req.getIsOverride() + && "clone-snap-1".equals(req.getSourcePath()) + && "vm-disk.qcow2".equals(req.getDestinationPath()) + && req.getVolume() != null + && "flexvol-uuid-1".equals(req.getVolume().getUuid()) + && "flexvol1".equals(req.getVolume().getName()))); + } + + @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..78fe34cd3cb1 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 @@ -30,6 +30,7 @@ import org.apache.cloudstack.storage.feign.model.Lun; import org.apache.cloudstack.storage.feign.model.LunMap; import org.apache.cloudstack.storage.feign.model.OntapStorage; +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; @@ -59,6 +60,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 +1804,40 @@ void testEnsureLunMapped_ExistingMapping_ReturnsExistingNumber() { verify(sanFeignClient, never()).createLunMap(any(), anyBoolean(), any(LunMap.class)); } } + + @Test + void testRevertSnapshotForCloudStackVolume_UsesLunCloneWithOverride() { + JobResponse jobResponse = new JobResponse(); + org.apache.cloudstack.storage.feign.model.Job job = new org.apache.cloudstack.storage.feign.model.Job(); + job.setUuid("job-uuid-1"); + jobResponse.setJob(job); + when(sanFeignClient.cloneLun(eq(authHeader), any(Lun.class))).thenReturn(jobResponse); + + try (MockedStatic utilityMock = mockStatic(OntapStorageUtils.class)) { + utilityMock.when(() -> OntapStorageUtils.generateAuthHeader("admin", "password")) + .thenReturn(authHeader); + + JobResponse result = unifiedSANStrategy.revertSnapshotForCloudStackVolume( + "clone-snap-1", "flexvol-uuid-1", "clone-lun-uuid-1", "dest-lun-1", "clone-lun-uuid-1", "flexvol1"); + + assertNotNull(result); + verify(sanFeignClient).cloneLun(eq(authHeader), argThat(lun -> + lun != null + && Boolean.TRUE.equals(lun.getIsOverride()) + && "dest-lun-1".equals(lun.getName()) + && lun.getClone() != null + && lun.getClone().getSource() != null + && "clone-lun-uuid-1".equals(lun.getClone().getSource().getUuid()) + && lun.getLocation() != null + && lun.getLocation().getVolume() != null + && "flexvol1".equals(lun.getLocation().getVolume().getName()) + )); + } + } + + @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..cd095cccefcd 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 @@ -592,11 +592,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); }