Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -67,7 +67,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;
Expand Down Expand Up @@ -98,6 +97,7 @@ public Map<String, String> getCapabilities() {
Map<String, String> mapCapabilities = new HashMap<>();
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_REVERT_VOLUME_TO_SNAPSHOT.toString(), Boolean.TRUE.toString());
return mapCapabilities;
}

Expand Down Expand Up @@ -670,8 +670,8 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback<CreateCm

SnapshotObjectTO snapshotObjectTo = (SnapshotObjectTO) snapshot.getTO();

// Build snapshot name using volume name and snapshot UUID
String snapshotName = buildSnapshotName(volumeInfo.getName(), snapshot.getUuid());
// Preserve CloudStack UI snapshot name with stable uniqueness suffix.
String snapshotName = buildSnapshotName(snapshot.getName(), snapshot.getId());

// Resolve the volume path for storing in snapshot details (for revert operation)
String volumePath = resolveVolumePathOnOntap(volumeVO, protocol, poolDetails);
Expand All @@ -684,6 +684,7 @@ public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback<CreateCm
if (lunUUID == null) {
throw new CloudRuntimeException("LUN UUID not found for iSCSI volume " + volumeVO.getId());
}
lunUuid = lunUUID;
}

// Create FlexVolume snapshot via ONTAP REST API
Expand Down Expand Up @@ -789,15 +790,8 @@ private String resolveSnapshotUuid(SnapshotFeignClient snapshotClient, String au
* specific file (NFS) or LUN (iSCSI) from the FlexVolume snapshot directly
* via ONTAP REST API, without involving the hypervisor agent.</p>
*
* <p><b>Protocol-specific handling (delegated to strategy classes):</b></p>
* <ul>
* <li><b>NFS (UnifiedNASStrategy):</b> Uses the single-file restore API:
* {@code POST /api/storage/volumes/{volume_uuid}/snapshots/{snapshot_uuid}/files/{file_path}/restore}
* Restores the QCOW2 file from the FlexVolume snapshot to its original location.</li>
* <li><b>iSCSI (UnifiedSANStrategy):</b> Uses the LUN restore API:
* {@code POST /api/storage/luns/{lun.uuid}/restore}
* Restores the LUN data from the snapshot to the specified destination path.</li>
* </ul>
* <p>Both NFS and iSCSI delegate to CLI-based SFSR:
* {@code POST /api/private/cli/volume/snapshot/restore-file}</p>
*/
@Override
public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snapshotOnPrimaryStore,
Expand Down Expand Up @@ -847,17 +841,7 @@ public void revertSnapshot(SnapshotInfo snapshotOnImageStore, SnapshotInfo snaps
JobResponse jobResponse = storageStrategy.revertSnapshotForCloudStackVolume(
snapshotName, flexVolUuid, ontapSnapshotUuid, volumePath, lunUuid, flexVolName);

if (jobResponse == null || jobResponse.getJob() == null) {
throw new CloudRuntimeException("Failed to initiate restore from snapshot [" +
snapshotName + "]");
}

// Poll for job completion (use longer timeout for large LUNs/files)
Boolean jobSucceeded = storageStrategy.jobPollForSuccess(jobResponse.getJob().getUuid(), 60, 2000);
if (!jobSucceeded) {
throw new CloudRuntimeException("Restore job failed for snapshot [" +
snapshotName + "]");
}
storageStrategy.executeCliSfsrRestore(jobResponse, "revert snapshot [" + snapshotName + "]");

logger.info("revertSnapshot: Successfully restored {} [{}] from snapshot [{}]",
ProtocolType.ISCSI.name().equalsIgnoreCase(protocol) ? "LUN" : "file",
Expand Down Expand Up @@ -975,18 +959,10 @@ private CloudStackVolume createDeleteCloudStackVolumeRequest(StoragePool storage
// ──────────────────────────────────────────────────────────────────────────

/**
* Builds a snapshot name with proper length constraints.
* Format: {@code <volumeName>-<snapshotUuid>}
* Builds an ONTAP-safe snapshot name from the CloudStack UI name with uniqueness suffix.
*/
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;
private String buildSnapshotName(String cloudStackSnapshotName, long snapshotId) {
return OntapStorageUtils.buildOntapSnapshotName(cloudStackSnapshotName, "cs" + snapshotId);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,96 @@ JobResponse restoreFileFromSnapshot(@Param("authHeader") String authHeader,
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
JobResponse restoreFileFromSnapshotCli(@Param("authHeader") String authHeader,
CliSnapshotRestoreRequest request);

/**
* Creates a consistency group.
*
* <p>ONTAP REST: {@code POST /api/application/consistency-groups}</p>
*
* @param authHeader Basic auth header
* @param request consistency group create request body
* @return JobResponse containing the async job reference
*/
@RequestLine("POST /api/application/consistency-groups")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
JobResponse createConsistencyGroup(@Param("authHeader") String authHeader,
Map<String, Object> request);

/**
* Lists consistency groups.
*
* <p>ONTAP REST: {@code GET /api/application/consistency-groups}</p>
*
* @param authHeader Basic auth header
* @param queryParams Optional query parameters
* @return Paginated consistency group records
*/
@RequestLine("GET /api/application/consistency-groups")
@Headers({"Authorization: {authHeader}"})
OntapResponse<Map<String, Object>> getConsistencyGroups(@Param("authHeader") String authHeader,
@QueryMap Map<String, Object> queryParams);

/**
* Creates (starts) a consistency group snapshot.
*
* <p>ONTAP REST: {@code POST /api/application/consistency-groups/{cgUuid}/snapshots}</p>
*
* @param authHeader Basic auth header
* @param cgUuid consistency group UUID
* @param request snapshot start request body
* @return JobResponse containing the async job reference
*/
@RequestLine("POST /api/application/consistency-groups/{cgUuid}/snapshots")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
JobResponse createConsistencyGroupSnapshot(@Param("authHeader") String authHeader,
@Param("cgUuid") String cgUuid,
Map<String, Object> request);

/**
* Lists snapshots for a consistency group.
*
* <p>ONTAP REST: {@code GET /api/application/consistency-groups/{cgUuid}/snapshots}</p>
*
* @param authHeader Basic auth header
* @param cgUuid consistency group UUID
* @param queryParams Optional query parameters
* @return Paginated consistency group snapshot records
*/
@RequestLine("GET /api/application/consistency-groups/{cgUuid}/snapshots")
@Headers({"Authorization: {authHeader}"})
OntapResponse<Map<String, Object>> getConsistencyGroupSnapshots(@Param("authHeader") String authHeader,
@Param("cgUuid") String cgUuid,
@QueryMap Map<String, Object> queryParams);

/**
* Commits a started consistency group snapshot.
*
* <p>ONTAP REST: {@code PATCH /api/application/consistency-groups/{cgUuid}/snapshots/{snapshotUuid}}</p>
*
* @param authHeader Basic auth header
* @param cgUuid consistency group UUID
* @param snapshotUuid consistency group snapshot UUID
* @param request commit request body
* @return JobResponse containing the async job reference
*/
@RequestLine("PATCH /api/application/consistency-groups/{cgUuid}/snapshots/{snapshotUuid}")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
JobResponse commitConsistencyGroupSnapshot(@Param("authHeader") String authHeader,
@Param("cgUuid") String cgUuid,
@Param("snapshotUuid") String snapshotUuid,
Map<String, Object> request);

/**
* Deletes a consistency group.
*
* <p>ONTAP REST: {@code DELETE /api/application/consistency-groups/{cgUuid}}</p>
*
* @param authHeader Basic auth header
* @param cgUuid consistency group UUID
* @return JobResponse containing the async job reference
*/
@RequestLine("DELETE /api/application/consistency-groups/{cgUuid}")
@Headers({"Authorization: {authHeader}"})
JobResponse deleteConsistencyGroup(@Param("authHeader") String authHeader,
@Param("cgUuid") String cgUuid);
}
Original file line number Diff line number Diff line change
Expand Up @@ -527,15 +527,13 @@ public String getNetworkInterface() {
abstract public CloudStackVolume getCloudStackVolume(Map<String, String> cloudStackVolumeMap);

/**
* Reverts a CloudStack volume to a snapshot using protocol-specific ONTAP APIs.
* Reverts a CloudStack volume to a snapshot using ONTAP CLI-based Single File Snap Restore (SFSR).
*
* <p>This method encapsulates the snapshot revert behavior based on protocol:</p>
* <ul>
* <li><b>iSCSI/FC:</b> Uses {@code POST /api/storage/luns/{lun.uuid}/restore}
* to restore LUN data from the FlexVolume snapshot.</li>
* <li><b>NFS:</b> Uses {@code POST /api/storage/volumes/{vol.uuid}/snapshots/{snap.uuid}/files/{path}/restore}
* to restore a single file from the FlexVolume snapshot.</li>
* </ul>
* <p>Both NFS and iSCSI use the CLI passthrough API:
* {@code POST /api/private/cli/volume/snapshot/restore-file}</p>
*
* <p>Callers should invoke {@link #executeCliSfsrRestore(JobResponse, String)} after this
* method returns to poll the async job when present, or treat a missing job as synchronous success.</p>
*
* @param snapshotName The ONTAP FlexVolume snapshot name
* @param flexVolUuid The FlexVolume UUID containing the snapshot
Expand Down Expand Up @@ -681,4 +679,45 @@ public Boolean jobPollForSuccess(String jobUUID, int maxRetries, int sleepTimeIn
}
return true;
}

/**
* Polls an ONTAP async job when the API response includes a job reference.
*
* <p>When no job is returned (common for CLI passthrough SFSR on synchronous completion),
* the operation is treated as successful after HTTP 2xx.</p>
*
* @param response ONTAP job response (may be null or without a job)
* @param operationName label for logging and error messages
*/
public void pollJobIfPresent(JobResponse response, String operationName) {
pollJobIfPresent(response, operationName,
OntapStorageConstants.ONTAP_CG_JOB_MAX_RETRIES,
OntapStorageConstants.ONTAP_CG_JOB_POLL_INTERVAL_MS);
}

/**
* Polls an ONTAP async job when present, using caller-supplied retry settings.
*/
public void pollJobIfPresent(JobResponse response, String operationName,
int maxRetries, int pollIntervalMs) {
if (response == null || response.getJob() == null || response.getJob().getUuid() == null) {
logger.debug("pollJobIfPresent: No async job returned for operation [{}], continuing without polling",
operationName);
return;
}
Boolean success = jobPollForSuccess(response.getJob().getUuid(), maxRetries, pollIntervalMs);
if (!Boolean.TRUE.equals(success)) {
throw new CloudRuntimeException("ONTAP operation failed: " + operationName);
}
}

/**
* Completes CLI-based SFSR ({@code restore-file}) orchestration: poll job when returned,
* otherwise accept synchronous success.
*/
public void executeCliSfsrRestore(JobResponse response, String operationName) {
pollJobIfPresent(response, operationName,
OntapStorageConstants.ONTAP_SFSR_JOB_MAX_RETRIES,
OntapStorageConstants.ONTAP_SFSR_JOB_POLL_INTERVAL_MS);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ public class OntapStorageConstants {
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 String ONTAP_TEMP_CG_PREFIX = "cs-temp-cg-";
public static final int ONTAP_CG_JOB_MAX_RETRIES = 60;
public static final int ONTAP_CG_JOB_POLL_INTERVAL_MS = 2000;
public static final int ONTAP_SFSR_JOB_MAX_RETRIES = 60;
public static final int ONTAP_SFSR_JOB_POLL_INTERVAL_MS = 2000;

/** vm_snapshot_details key for ONTAP FlexVolume-level VM snapshots. */
public static final String ONTAP_FLEXVOL_SNAPSHOT = "ontapFlexVolSnapshot";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,46 @@ public static String getLunName(String volName, String lunName) {
return OntapStorageConstants.VOLUME_PATH_PREFIX + volName + OntapStorageConstants.SLASH + lunName;
}

/**
* Builds an ONTAP-safe name token from user-provided snapshot text.
*/
public static String getOntapCloneName(String cloudStackSnapshotName) {
if (cloudStackSnapshotName == null || cloudStackSnapshotName.trim().isEmpty()) {
throw new InvalidParameterValueException("Snapshot name cannot be null or blank");
}
String normalized = cloudStackSnapshotName.replaceAll("[^a-zA-Z0-9_]", "_");
if (normalized.isEmpty()) {
normalized = "snapshot";
}
if (!Character.isLetter(normalized.charAt(0))) {
normalized = "s_" + normalized;
}
if (normalized.length() > OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH) {
normalized = normalized.substring(0, OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH);
}
return normalized;
}

/**
* Builds an ONTAP-safe snapshot name that preserves the CloudStack UI snapshot name
* and appends a uniqueness suffix.
*/
public static String buildOntapSnapshotName(String cloudStackSnapshotName, String uniquenessSuffix) {
String normalizedBase = (cloudStackSnapshotName == null || cloudStackSnapshotName.trim().isEmpty())
? "snapshot"
: getOntapCloneName(cloudStackSnapshotName);
String suffix = (uniquenessSuffix == null || uniquenessSuffix.isEmpty())
? ""
: "_" + uniquenessSuffix.replaceAll("[^a-zA-Z0-9_]", "_");
int maxLength = OntapStorageConstants.MAX_SNAPSHOT_NAME_LENGTH;
int maxBaseLength = maxLength - suffix.length();
if (maxBaseLength <= 0) {
return normalizedBase.substring(0, maxLength);
}
if (normalizedBase.length() > maxBaseLength) {
normalizedBase = normalizedBase.substring(0, maxBaseLength);
}
return normalizedBase + suffix;
}

}
Loading
Loading