Skip to content

Commit 96377a2

Browse files
CSTACKEC-18_2: revertsnapshot workflow using private cli REST endpoint
1 parent cf7d0b0 commit 96377a2

5 files changed

Lines changed: 190 additions & 27 deletions

File tree

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/driver/OntapPrimaryDatastoreDriver.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -830,16 +830,16 @@ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snaps
830830

831831
StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(poolDetails);
832832

833-
// Prepare protocol-specific parameters
834-
String lunUuid = null;
835-
String flexVolName = null;
833+
// Get the FlexVolume name (required for CLI-based restore API for all protocols)
834+
String flexVolName = poolDetails.get(Constants.VOLUME_NAME);
835+
if (flexVolName == null || flexVolName.isEmpty()) {
836+
throw new CloudRuntimeException("revertSnapshot: FlexVolume name not found in pool details for pool " + poolId);
837+
}
836838

839+
// Prepare protocol-specific parameters (lunUuid is only needed for backward compatibility)
840+
String lunUuid = null;
837841
if (ProtocolType.ISCSI.name().equalsIgnoreCase(protocol)) {
838842
lunUuid = getSnapshotDetail(snapshotId, Constants.LUN_DOT_UUID);
839-
if (lunUuid == null || lunUuid.isEmpty()) {
840-
throw new CloudRuntimeException("revertSnapshot: LUN UUID not found in snapshot details for iSCSI snapshot " + snapshotId);
841-
}
842-
flexVolName = poolDetails.get(Constants.VOLUME_NAME);
843843
}
844844

845845
// Delegate to strategy class for protocol-specific restore

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/feign/client/SnapshotFeignClient.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import feign.Param;
2323
import feign.QueryMap;
2424
import feign.RequestLine;
25+
import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
2526
import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot;
2627
import org.apache.cloudstack.storage.feign.model.SnapshotFileRestoreRequest;
2728
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
@@ -151,4 +152,33 @@ JobResponse restoreFileFromSnapshot(@Param("authHeader") String authHeader,
151152
@Param("snapshotUuid") String snapshotUuid,
152153
@Param("filePath") String filePath,
153154
SnapshotFileRestoreRequest request);
155+
156+
/**
157+
* Restores a single file or LUN from a FlexVolume snapshot using the CLI native API.
158+
*
159+
* <p>ONTAP REST (CLI passthrough):
160+
* {@code POST /api/private/cli/volume/snapshot/restore-file}</p>
161+
*
162+
* <p>This CLI-based API is more reliable and works for both NFS files and iSCSI LUNs.
163+
* The request body contains all required parameters: vserver, volume, snapshot, and path.</p>
164+
*
165+
* <p>Example payload:
166+
* <pre>
167+
* {
168+
* "vserver": "vs0",
169+
* "volume": "rajiv_ONTAP_SP1",
170+
* "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
171+
* "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
172+
* }
173+
* </pre>
174+
* </p>
175+
*
176+
* @param authHeader Basic auth header
177+
* @param request CLI snapshot restore request containing vserver, volume, snapshot, and path
178+
* @return JobResponse containing the async job reference (if applicable)
179+
*/
180+
@RequestLine("POST /api/private/cli/volume/snapshot/restore-file")
181+
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
182+
JobResponse restoreFileFromSnapshotCli(@Param("authHeader") String authHeader,
183+
CliSnapshotRestoreRequest request);
154184
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.cloudstack.storage.feign.model;
20+
21+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
22+
import com.fasterxml.jackson.annotation.JsonInclude;
23+
import com.fasterxml.jackson.annotation.JsonProperty;
24+
25+
/**
26+
* Request body for the ONTAP CLI-based Snapshot File Restore API.
27+
*
28+
* <p>ONTAP REST endpoint (CLI passthrough):
29+
* {@code POST /api/private/cli/volume/snapshot/restore-file}</p>
30+
*
31+
* <p>This API restores a single file or LUN from a FlexVolume snapshot to a
32+
* specified destination path using the CLI native implementation.
33+
* It works for both NFS files and iSCSI LUNs.</p>
34+
*
35+
* <p>Example payload:
36+
* <pre>
37+
* {
38+
* "vserver": "vs0",
39+
* "volume": "rajiv_ONTAP_SP1",
40+
* "snapshot": "DATA-3-428726fe-7440-4b41-8d47-3f654e5d9814",
41+
* "path": "/d266bb2c-d479-47ad-81c3-a070e8bb58c0"
42+
* }
43+
* </pre>
44+
* </p>
45+
*/
46+
@JsonIgnoreProperties(ignoreUnknown = true)
47+
@JsonInclude(JsonInclude.Include.NON_NULL)
48+
public class CliSnapshotRestoreRequest {
49+
50+
@JsonProperty("vserver")
51+
private String vserver;
52+
53+
@JsonProperty("volume")
54+
private String volume;
55+
56+
@JsonProperty("snapshot")
57+
private String snapshot;
58+
59+
@JsonProperty("path")
60+
private String path;
61+
62+
public CliSnapshotRestoreRequest() {
63+
}
64+
65+
/**
66+
* Creates a CLI snapshot restore request.
67+
*
68+
* @param vserver The SVM (vserver) name
69+
* @param volume The FlexVolume name
70+
* @param snapshot The snapshot name
71+
* @param path The file/LUN path to restore (e.g., "/uuid.qcow2" or "/lun_name")
72+
*/
73+
public CliSnapshotRestoreRequest(String vserver, String volume, String snapshot, String path) {
74+
this.vserver = vserver;
75+
this.volume = volume;
76+
this.snapshot = snapshot;
77+
this.path = path;
78+
}
79+
80+
public String getVserver() {
81+
return vserver;
82+
}
83+
84+
public void setVserver(String vserver) {
85+
this.vserver = vserver;
86+
}
87+
88+
public String getVolume() {
89+
return volume;
90+
}
91+
92+
public void setVolume(String volume) {
93+
this.volume = volume;
94+
}
95+
96+
public String getSnapshot() {
97+
return snapshot;
98+
}
99+
100+
public void setSnapshot(String snapshot) {
101+
this.snapshot = snapshot;
102+
}
103+
104+
public String getPath() {
105+
return path;
106+
}
107+
108+
public void setPath(String path) {
109+
this.path = path;
110+
}
111+
112+
@Override
113+
public String toString() {
114+
return "CliSnapshotRestoreRequest{" +
115+
"vserver='" + vserver + '\'' +
116+
", volume='" + volume + '\'' +
117+
", snapshot='" + snapshot + '\'' +
118+
", path='" + path + '\'' +
119+
'}';
120+
}
121+
}

plugins/storage/volume/ontap/src/main/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategy.java

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
3333
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
3434
import org.apache.cloudstack.storage.feign.client.SnapshotFeignClient;
35+
import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
3536
import org.apache.cloudstack.storage.feign.model.FlexVolSnapshot;
36-
import org.apache.cloudstack.storage.feign.model.SnapshotFileRestoreRequest;
3737
import org.apache.cloudstack.storage.feign.model.response.JobResponse;
3838
import org.apache.cloudstack.storage.feign.model.response.OntapResponse;
3939
import org.apache.cloudstack.storage.service.StorageStrategy;
@@ -157,8 +157,10 @@ public StrategyPriority canHandle(VMSnapshot vmSnapshot) {
157157

158158
// For new snapshots, check if Disk-only and all volumes on ONTAP
159159
if (vmSnapshotVO.getType() != VMSnapshot.Type.Disk) {
160-
logger.error("canHandle: ONTAP VM snapshot strategy cannot handle memory snapshots for VM [{}]", vmSnapshot.getVmId());
161-
throw new CloudRuntimeException("ONTAP VM snapshot strategy cannot handle memory snapshots for VM [" + vmSnapshot.getVmId() + "]");
160+
// Memory snapshots are not supported by ONTAP strategy - return CANT_HANDLE
161+
// so other strategies can be tried or proper error handling can occur
162+
logger.debug("canHandle: ONTAP VM snapshot strategy cannot handle memory snapshots for VM [{}]", vmSnapshot.getVmId());
163+
return StrategyPriority.CANT_HANDLE;
162164
}
163165

164166
if (allVolumesOnOntapManagedStorage(vmSnapshot.getVmId())) {
@@ -775,14 +777,14 @@ void deleteFlexVolSnapshots(List<VMSnapshotDetailsVO> flexVolDetails) {
775777
}
776778

777779
/**
778-
* Reverts all volumes of a VM snapshot using ONTAP Snapshot File Restore.
780+
* Reverts all volumes of a VM snapshot using ONTAP CLI-based Snapshot File Restore.
779781
*
780782
* <p>Instead of restoring the entire FlexVolume to a snapshot (which would affect
781783
* other VMs/files on the same FlexVol), this method restores <b>only the individual
782-
* files or LUNs</b> belonging to this VM using the dedicated ONTAP snapshot file
784+
* files or LUNs</b> belonging to this VM using the dedicated ONTAP CLI snapshot file
783785
* restore API:</p>
784786
*
785-
* <p>{@code POST /api/storage/volumes/{volume.uuid}/snapshots/{snapshot.uuid}/files/{file.path}/restore}</p>
787+
* <p>{@code POST /api/private/cli/volume/snapshot/restore-file}</p>
786788
*
787789
* <p>For each persisted detail row (one per CloudStack volume):</p>
788790
* <ul>
@@ -806,31 +808,40 @@ void revertFlexVolSnapshots(List<VMSnapshotDetailsVO> flexVolDetails) {
806808
SnapshotFeignClient snapshotClient = storageStrategy.getSnapshotFeignClient();
807809
String authHeader = storageStrategy.getAuthHeader();
808810

809-
// Prepare the file path for ONTAP API (ensure it starts with "/")
811+
// Get SVM name and FlexVolume name from pool details
812+
String svmName = poolDetails.get(Constants.SVM_NAME);
813+
String flexVolName = poolDetails.get(Constants.VOLUME_NAME);
814+
815+
if (svmName == null || svmName.isEmpty()) {
816+
throw new CloudRuntimeException("revertFlexVolSnapshots: SVM name not found in pool details for pool [" + detail.poolId + "]");
817+
}
818+
if (flexVolName == null || flexVolName.isEmpty()) {
819+
throw new CloudRuntimeException("revertFlexVolSnapshots: FlexVolume name not found in pool details for pool [" + detail.poolId + "]");
820+
}
821+
822+
// The path must start with "/" for the ONTAP CLI API
810823
String ontapFilePath = detail.volumePath.startsWith("/") ? detail.volumePath : "/" + detail.volumePath;
811824

812-
logger.info("revertFlexVolSnapshots: Restoring volume [{}] from FlexVol snapshot [{}] (uuid={}) on FlexVol [{}] (protocol={})",
813-
ontapFilePath, detail.snapshotName, detail.snapshotUuid,
814-
detail.flexVolUuid, detail.protocol);
825+
logger.info("revertFlexVolSnapshots: Restoring volume [{}] from FlexVol snapshot [{}] on FlexVol [{}] (protocol={})",
826+
ontapFilePath, detail.snapshotName, flexVolName, detail.protocol);
815827

816-
// POST /api/storage/volumes/{vol}/snapshots/{snap}/files/{path}/restore
817-
// with body: { "destination_path": "<volumePath>" }
818-
SnapshotFileRestoreRequest restoreRequest = new SnapshotFileRestoreRequest(detail.volumePath);
828+
// Use CLI-based restore API: POST /api/private/cli/volume/snapshot/restore-file
829+
CliSnapshotRestoreRequest restoreRequest = new CliSnapshotRestoreRequest(
830+
svmName, flexVolName, detail.snapshotName, ontapFilePath);
819831

820-
JobResponse jobResponse = snapshotClient.restoreFileFromSnapshot(
821-
authHeader, detail.flexVolUuid, detail.snapshotUuid, ontapFilePath, restoreRequest);
832+
JobResponse jobResponse = snapshotClient.restoreFileFromSnapshotCli(authHeader, restoreRequest);
822833

823834
if (jobResponse != null && jobResponse.getJob() != null) {
824835
Boolean success = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2);
825836
if (!success) {
826837
throw new CloudRuntimeException("Snapshot file restore failed for volume path [" +
827838
ontapFilePath + "] from snapshot [" + detail.snapshotName +
828-
"] on FlexVol [" + detail.flexVolUuid + "]");
839+
"] on FlexVol [" + flexVolName + "]");
829840
}
830841
}
831842

832843
logger.info("revertFlexVolSnapshots: Successfully restored volume [{}] from snapshot [{}] on FlexVol [{}]",
833-
ontapFilePath, detail.snapshotName, detail.flexVolUuid);
844+
ontapFilePath, detail.snapshotName, flexVolName);
834845
}
835846
}
836847

plugins/storage/volume/ontap/src/test/java/org/apache/cloudstack/storage/vmsnapshot/OntapVMSnapshotStrategyTest.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,12 +227,13 @@ void testCanHandle_AllocatedDiskType_AllVolumesOnOntap_ReturnsHighest() {
227227
}
228228

229229
@Test
230-
void testCanHandle_AllocatedDiskAndMemoryType_ThrowsException() {
230+
void testCanHandle_AllocatedDiskAndMemoryType_ReturnsCantHandle() {
231231
VMSnapshotVO vmSnapshot = createMockVmSnapshot(VMSnapshot.State.Allocated, VMSnapshot.Type.DiskAndMemory);
232232
when(vmSnapshot.getVmId()).thenReturn(VM_ID);
233233

234-
CloudRuntimeException ex = assertThrows(CloudRuntimeException.class, () -> strategy.canHandle(vmSnapshot));
235-
assertEquals(true, ex.getMessage().contains("Memory snapshots are not supported") || ex.getMessage().contains("cannot handle memory snapshots"));
234+
StrategyPriority result = strategy.canHandle(vmSnapshot);
235+
236+
assertEquals(StrategyPriority.CANT_HANDLE, result);
236237
}
237238

238239
@Test

0 commit comments

Comments
 (0)