Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
f42552b
CSTACKEX-18_2: NFS3 snapshot changes
rajiv-jain-netapp Feb 19, 2026
8894248
CSTACK-18_2: fixing junit dependent changes
rajiv-jain-netapp Feb 19, 2026
3f0019a
STACK-18_2: fixes
rajiv-jain-netapp Feb 20, 2026
9b79f46
CSTACKEX-18_2: adding VM snapshot logic
rajiv-jain-netapp Feb 20, 2026
7a0d61e
CSTACKEX-18_2: fix junit issues
rajiv-jain-netapp Feb 20, 2026
7c3419e
CSTACKEX-18_2: fixes for vm snapshot workflow
rajiv-jain-netapp Feb 21, 2026
d2b6a27
CSTACKEX-18_2: fixing the behaviour for the VM level snapshot when qu…
rajiv-jain-netapp Feb 21, 2026
c5d5428
CSTACKEX-18_2: incorporating the review comments.
rajiv-jain-netapp Feb 24, 2026
3f18c11
CSTACKEX-18_2: transient fixes post incorporating the comments
rajiv-jain-netapp Feb 24, 2026
723561b
CSTACKEX-18_2: Incorporate review comments
rajiv-jain-netapp Feb 24, 2026
09968db
CSTACKEX-18_2: quiecing VM would be done based on user input for VM l…
rajiv-jain-netapp Feb 25, 2026
0a1a9c4
CSTACKEX-18_2: ONTAP plugin can not handle memory snapshot with stora…
rajiv-jain-netapp Feb 26, 2026
49df4c3
CSTACKEX-18_2: junit fix
rajiv-jain-netapp Feb 26, 2026
776b9a2
CSTACKEX-18_2: junit fix2
rajiv-jain-netapp Feb 26, 2026
c04e223
CSTACKEX-18_2: ensure that ONTAP volume related calls are served by c…
rajiv-jain-netapp Feb 26, 2026
186e59b
CSTACKEX-18_2: using flexvolume snapshot to get snapshot workflows fo…
rajiv-jain-netapp Feb 26, 2026
1020a2c
CSTACKEX-18_2: using flexvolume snapshot even for CS volume snapshot …
rajiv-jain-netapp Feb 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ public class KvmFileBasedStorageVmSnapshotStrategy extends StorageVMSnapshotStra

private static final List<Storage.StoragePoolType> supportedStoragePoolTypes = List.of(Storage.StoragePoolType.Filesystem, Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.SharedMountPoint);

private static final String ONTAP_PROVIDER_NAME = "NetApp ONTAP";

@Inject
protected SnapshotDataStoreDao snapshotDataStoreDao;

Expand Down Expand Up @@ -325,6 +327,11 @@ public StrategyPriority canHandle(Long vmId, Long rootPoolId, boolean snapshotMe
List<VolumeVO> volumes = volumeDao.findByInstance(vmId);
for (VolumeVO volume : volumes) {
StoragePoolVO storagePoolVO = storagePool.findById(volume.getPoolId());
if (storagePoolVO.isManaged() && ONTAP_PROVIDER_NAME.equals(storagePoolVO.getStorageProviderName())) {
logger.debug(String.format("%s as the VM has a volume on ONTAP managed storage pool [%s]. " +
"ONTAP managed storage has its own dedicated VM snapshot strategy.", cantHandleLog, storagePoolVO.getName()));
return StrategyPriority.CANT_HANDLE;
}
if (!supportedStoragePoolTypes.contains(storagePoolVO.getPoolType())) {
logger.debug(String.format("%s as the VM has a volume that is in a storage with unsupported type [%s].", cantHandleLog, storagePoolVO.getPoolType()));
return StrategyPriority.CANT_HANDLE;
Expand Down Expand Up @@ -503,8 +510,9 @@ protected VMSnapshot takeVmSnapshotInternal(VMSnapshot vmSnapshot, Map<VolumeInf
return processCreateVmSnapshotAnswer(vmSnapshot, volumeInfoToSnapshotObjectMap, createDiskOnlyVMSnapshotAnswer, userVm, vmSnapshotVO, virtualSize, parentSnapshotVo);
}

logger.error("Disk-only VM snapshot for VM [{}] failed{}.", userVm.getUuid(), answer != null ? " due to" + answer.getDetails() : "");
throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed.", userVm.getUuid()));
String details = answer != null ? answer.getDetails() : String.format("No answer received from host [%s]. The host may be unreachable.", hostId);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why this change is required?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am returning the failure reasons through the exception.

logger.error("Disk-only VM snapshot for VM [{}] failed due to: {}.", userVm.getUuid(), details);
throw new CloudRuntimeException(String.format("Disk-only VM snapshot for VM [%s] failed due to: %s.", userVm.getUuid(), details));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ protected Answer takeDiskOnlyVmSnapshotOfRunningVm(CreateDiskOnlyVmSnapshotComma
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null);
}
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, e.getMessage(), null);
} catch (Exception e) {
String errorMsg = String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage());
logger.error(errorMsg, e);
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false, errorMsg, null);
} finally {
if (dm != null) {
try {
Expand Down Expand Up @@ -146,21 +150,13 @@ protected Answer takeDiskOnlyVmSnapshotOfStoppedVm(CreateDiskOnlyVmSnapshotComma
}
} catch (LibvirtException | QemuImgException e) {
logger.error("Exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());

if (volSizeAndNewPath == null) {
continue;
}
try {
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
} catch (IOException ex) {
logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
}
}
cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr);
return new Answer(cmd, e);
} catch (Exception e) {
logger.error("Unexpected exception while creating disk-only VM snapshot for VM [{}]. Deleting leftover deltas.", vmName, e);
cleanupLeftoverDeltas(volumeObjectTos, mapVolumeToSnapshotSizeAndNewVolumePath, storagePoolMgr);
return new CreateDiskOnlyVmSnapshotAnswer(cmd, false,
String.format("Creation of disk-only VM snapshot for VM [%s] failed due to %s.", vmName, e.getMessage()), null);
}

return new CreateDiskOnlyVmSnapshotAnswer(cmd, true, null, mapVolumeToSnapshotSizeAndNewVolumePath);
Expand Down Expand Up @@ -192,6 +188,23 @@ protected Pair<String, Map<String, Pair<Long, String>>> createSnapshotXmlAndNewV
return new Pair<>(snapshotXml, volumeObjectToNewPathMap);
}

protected void cleanupLeftoverDeltas(List<VolumeObjectTO> volumeObjectTos, Map<String, Pair<Long, String>> mapVolumeToSnapshotSizeAndNewVolumePath, KVMStoragePoolManager storagePoolMgr) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this refactoring somehow help our plugin functionality?

for (VolumeObjectTO volumeObjectTO : volumeObjectTos) {
Pair<Long, String> volSizeAndNewPath = mapVolumeToSnapshotSizeAndNewVolumePath.get(volumeObjectTO.getUuid());
PrimaryDataStoreTO primaryDataStoreTO = (PrimaryDataStoreTO) volumeObjectTO.getDataStore();
KVMStoragePool kvmStoragePool = storagePoolMgr.getStoragePool(primaryDataStoreTO.getPoolType(), primaryDataStoreTO.getUuid());

if (volSizeAndNewPath == null) {
continue;
}
try {
Files.deleteIfExists(Path.of(kvmStoragePool.getLocalPathFor(volSizeAndNewPath.second())));
} catch (IOException ex) {
logger.warn("Tried to delete leftover snapshot at [{}] failed.", volSizeAndNewPath.second(), ex);
}
}
}

protected long getFileSize(String path) {
return new File(path).length();
}
Expand Down
10 changes: 10 additions & 0 deletions plugins/storage/volume/ontap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@
<artifactId>cloud-engine-storage-volume</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-engine-storage-snapshot</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.cloudstack</groupId>
<artifactId>cloud-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,15 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
@Inject private VMInstanceDao vmDao;
@Inject private VolumeDao volumeDao;
@Inject private VolumeDetailsDao volumeDetailsDao;

@Override
public Map<String, String> getCapabilities() {
s_logger.trace("OntapPrimaryDatastoreDriver: getCapabilities: Called");
Map<String, String> mapCapabilities = new HashMap<>();
// RAW managed initial implementation: snapshot features not yet supported
// TODO Set it to false once we start supporting snapshot feature
// Snapshot operations are handled by OntapSnapshotStrategy (volume-level)
// and OntapVMSnapshotStrategy (VM-level) using native ONTAP FlexVol snapshots.
// These capabilities are set to FALSE so that StorageSystemSnapshotStrategy
// does not attempt to handle ONTAP volumes through the driver path.
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.FALSE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.FALSE.toString());
return mapCapabilities;
Expand Down Expand Up @@ -524,7 +527,16 @@ public long getUsedIops(StoragePool storagePool) {

@Override
public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback<CreateCmdResult> callback) {

// Snapshot operations are now handled by OntapSnapshotStrategy which intercepts
// at the strategy layer before the driver is ever called. This method should
// not be reached for ONTAP volumes. If it is, fail explicitly.
s_logger.error("takeSnapshot: This driver method should not be called. " +
"Snapshot operations for ONTAP are handled by OntapSnapshotStrategy. " +
"Snapshot id: " + snapshot.getId());
CreateCmdResult result = new CreateCmdResult(null, new Answer(null, false,
"takeSnapshot not supported via driver path; use OntapSnapshotStrategy"));
result.setResult("takeSnapshot not supported via driver path");
callback.complete(result);
}

@Override
Expand Down Expand Up @@ -622,4 +634,5 @@ private CloudStackVolume createDeleteCloudStackVolumeRequest(StoragePool storage
return cloudStackVolumeDeleteRequest;

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@

import feign.QueryMap;
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
import org.apache.cloudstack.storage.feign.model.FileClone;
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;
Expand All @@ -32,7 +34,7 @@
public interface NASFeignClient {

// File Operations
@RequestLine("GET /api/storage/volumes/{volumeUuid}/files/{path}")
@RequestLine("GET /api/storage/volumes/{volumeUuid}/files/{path}?return_metadata=true")
@Headers({"Authorization: {authHeader}"})
OntapResponse<FileInfo> getFileResponse(@Param("authHeader") String authHeader,
@Param("volumeUuid") String volumeUUID,
Expand All @@ -58,6 +60,11 @@ void createFile(@Param("authHeader") String authHeader,
@Param("path") String filePath,
FileInfo file);

@RequestLine("POST /api/storage/file/clone")
@Headers({"Authorization: {authHeader}"})
JobResponse cloneFile(@Param("authHeader") String authHeader,
FileClone fileClone);

// Export Policy Operations
@RequestLine("POST /api/protocols/nfs/export-policies")
@Headers({"Authorization: {authHeader}"})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* 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.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.
*
* <p>Maps to the ONTAP REST API endpoint:
* {@code /api/storage/volumes/{volume_uuid}/snapshots}</p>
*
* <p>FlexVolume snapshots are point-in-time, space-efficient copies of an entire
* FlexVolume. Unlike file-level clones, a single FlexVolume snapshot atomically
* captures <b>all</b> files/LUNs within the volume, making it ideal for VM-level
* snapshots when multiple CloudStack disks reside on the same FlexVolume.</p>
*/
public interface SnapshotFeignClient {

/**
* Creates a new snapshot for the specified FlexVolume.
*
* <p>ONTAP REST: {@code POST /api/storage/volumes/{volume_uuid}/snapshots}</p>
*
* @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.
*
* <p>ONTAP REST: {@code GET /api/storage/volumes/{volume_uuid}/snapshots}</p>
*
* @param authHeader Basic auth header
* @param volumeUuid UUID of the ONTAP FlexVolume
* @param queryParams Optional query parameters (e.g., {@code name}, {@code fields})
* @return Paginated response of FlexVolSnapshot records
*/
@RequestLine("GET /api/storage/volumes/{volumeUuid}/snapshots")
@Headers({"Authorization: {authHeader}"})
OntapResponse<FlexVolSnapshot> getSnapshots(@Param("authHeader") String authHeader,
@Param("volumeUuid") String volumeUuid,
@QueryMap Map<String, Object> queryParams);

/**
* Retrieves a specific snapshot by UUID.
*
* <p>ONTAP REST: {@code GET /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}</p>
*
* @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.
*
* <p>ONTAP REST: {@code DELETE /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}</p>
*
* @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.
*
* <p>ONTAP REST: {@code PATCH /api/storage/volumes/{volume_uuid}/snapshots/{uuid}}
* with body {@code {"restore": true}} triggers a snapshot restore operation.</p>
*
* <p><b>Note:</b> This is a destructive operation — all data written after the
* snapshot was taken will be lost.</p>
*
* @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.
*
* <p>ONTAP REST:
* {@code POST /api/storage/volumes/{volume_uuid}/snapshots/{snapshot_uuid}/files/{file_path}/restore}</p>
*
* <p>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.</p>
*
* @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);
}
Loading
Loading