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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ public class FileCloneRequest {
@JsonProperty("overwrite_destination")
private Boolean overwriteDestination;

/**
* Optional FlexVolume snapshot to clone from. When set, ONTAP clones {@code source_path}
* as it existed in that snapshot rather than from the live file/LUN.
*
* <p>Used by create-volume-from-snapshot (same FlexVol). Omitted for live template-cache clones.</p>
*/
@JsonProperty("snapshot")
private SnapshotRef snapshot;

public FileCloneRequest() {
}

Expand All @@ -55,6 +64,14 @@ public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePat
this.destinationPath = destinationPath;
}

public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePath, String destinationPath,
String snapshotName) {
this(flexVolUuid, flexVolName, sourcePath, destinationPath);
if (snapshotName != null && !snapshotName.isEmpty()) {
this.snapshot = new SnapshotRef(snapshotName);
}
}

public VolumeRef getVolume() {
return volume;
}
Expand Down Expand Up @@ -87,6 +104,14 @@ public void setOverwriteDestination(Boolean overwriteDestination) {
this.overwriteDestination = overwriteDestination;
}

public SnapshotRef getSnapshot() {
return snapshot;
}

public void setSnapshot(SnapshotRef snapshot) {
this.snapshot = snapshot;
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class VolumeRef {
Expand Down Expand Up @@ -122,10 +147,37 @@ public void setName(String name) {
}
}

/**
* Snapshot identity for {@code POST /api/storage/file/clone} when cloning from a FlexVol snapshot.
*/
@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;
}
}

@Override
public String toString() {
return "FileCloneRequest{volume=" + (volume != null ? volume.getUuid() : null)
+ ", sourcePath=" + sourcePath
+ ", destinationPath=" + destinationPath + "}";
+ ", destinationPath=" + destinationPath
+ ", snapshot=" + (snapshot != null ? snapshot.getName() : null) + "}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
import feign.FeignException;

import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;

/**
* Storage Strategy represents the communication path for all the ONTAP storage options
Expand Down Expand Up @@ -829,6 +830,32 @@ abstract public CloudStackVolume createTemplateCache(StoragePoolVO storagePool,
*/
abstract public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume);

/**
* Creates a new file/LUN in the same FlexVolume by cloning from a FlexVolume snapshot.
*
* <p><b>Product scope (v1):</b> same primary pool / FlexVol only. Cross-pool restore is
* descoped — operators may later {@code migrateVolume} if another pool is required.</p>
*
* <p>ONTAP backends (protocol-specific; each subclass builds its own request):</p>
* <ul>
* <li><b>NAS (NFS3)</b> — {@code POST /api/storage/file/clone} with {@code snapshot.name}</li>
* <li><b>SAN (iSCSI)</b> — {@code POST /api/storage/luns} with {@code clone.source.name} =
* {@code /vol/&lt;fv&gt;/.snapshot/&lt;snap&gt;/&lt;lun&gt;}</li>
* </ul>
*
* @param storagePool target CloudStack primary pool (same FlexVol as the snapshot)
* @param details pool details (SVM, FlexVol name/uuid, protocol, …)
* @param volumeInfo destination CloudStack volume being created
* @param sourceVolumePath snapshotted object path from {@code snapshot_details.VOLUME_PATH}
* @param snapshotName ONTAP FlexVol snapshot name from {@code snapshot_details}
* @return created CloudStackVolume with protocol-specific identity (LUN uuid or file path)
*/
abstract public CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storagePool,
Map<String, String> details,
VolumeInfo volumeInfo,
String sourceVolumePath,
String snapshotName);

/**
* Grows an existing backend object to {@code sizeInBytes}.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.apache.cloudstack.engine.subsystem.api.storage.EndPoint;
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.command.CreateObjectCommand;
import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
Expand Down Expand Up @@ -188,6 +189,76 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
}
}

/**
* Creates a new qcow2 (or other file) in the FlexVol by cloning from a FlexVolume snapshot
* via {@code POST /api/storage/file/clone} with {@code snapshot.name}.
*
* <p>Builds the NFS request and executes it (mirrors {@link #createTemplateCache}).
* SAN uses the LUN REST clone path instead.</p>
*
* <p><b>Combinations covered here:</b></p>
* <ul>
* <li>DATA or ROOT snapshot → new data volume (ROOT restore is never bootable as a volume;
* CloudStack still uses this path for {@code createVolume(snapshotid)}; bootable ROOT
* recovery remains {@code createTemplate} → deploy)</li>
* <li>Same pool / FlexVol only (v1)</li>
* <li>IOPS from snapshot_details are not applied here — see driver TODO</li>
* </ul>
*/
@Override
public CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storagePool, Map<String, String> details,
VolumeInfo volumeInfo, String sourceVolumePath,
String snapshotName) {
if (storagePool == null || details == null || volumeInfo == null) {
throw new CloudRuntimeException("Failed to clone file from snapshot, invalid request");
}
if (sourceVolumePath == null || sourceVolumePath.isEmpty()) {
throw new CloudRuntimeException("Failed to clone file from snapshot, source path is required");
}
if (snapshotName == null || snapshotName.isEmpty()) {
throw new CloudRuntimeException("Failed to clone file from snapshot, snapshot name is required");
}

String flexVolUuid = details.get(OntapStorageConstants.VOLUME_UUID);
String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
if (flexVolUuid == null || flexVolUuid.isEmpty()) {
throw new CloudRuntimeException("Failed to clone file from snapshot, FlexVolume uuid is missing from pool details");
}

String sourcePath = OntapStorageUtils.toFlexVolRelativePath(sourceVolumePath, flexVolName);
String destinationPath = OntapStorageUtils.toFlexVolRelativePath(volumeInfo.getUuid(), flexVolName);

logger.info("cloneCloudStackVolumeFromSnapshot [NFS]: Cloning file [{}] -> [{}] from snapshot [{}] on FlexVol [{}]",
sourcePath, destinationPath, snapshotName, flexVolName);
try {
FileCloneRequest request = new FileCloneRequest(flexVolUuid, flexVolName, sourcePath, destinationPath, snapshotName);
JobResponse jobResponse = nasFeignClient.cloneFile(getAuthHeader(), request);
pollJobIfPresent(jobResponse, "clone file from snapshot [" + snapshotName + "] [" + sourcePath
+ "] to [" + destinationPath + "]");

updateCloudStackVolumeMetadata(String.valueOf(storagePool.getId()), volumeInfo);

FileInfo clonedFile = new FileInfo();
clonedFile.setPath(destinationPath);

CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
clonedCloudStackVolume.setFile(clonedFile);
clonedCloudStackVolume.setDatastoreId(String.valueOf(storagePool.getId()));
clonedCloudStackVolume.setVolumeInfo(volumeInfo);
clonedCloudStackVolume.setSnapshotName(snapshotName);
return clonedCloudStackVolume;
} catch (FeignException e) {
logger.error("FeignException while cloning file from snapshot [{}], Status: {}, Exception: {}",
snapshotName, e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to clone file from snapshot: " + e.getMessage());
} catch (CloudRuntimeException e) {
throw e;
} catch (Exception e) {
logger.error("Exception while cloning file from snapshot [{}]: {}", snapshotName, e.getMessage());
throw new CloudRuntimeException("Failed to clone file from snapshot: " + e.getMessage());
}
}

/**
* Grows the cloned qcow2 to the requested size via a host-side {@code qemu-img resize}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.cloud.utils.exception.CloudRuntimeException;
import feign.FeignException;
import org.apache.cloudstack.engine.subsystem.api.storage.TemplateInfo;
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.Igroup;
Expand Down Expand Up @@ -260,6 +261,102 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
}
}

/**
* Creates a new LUN by cloning from a FlexVolume snapshot via the LUN REST API
* ({@code POST /api/storage/luns} with {@code clone.source.name} pointing into
* {@code /vol/&lt;fv&gt;/.snapshot/&lt;snap&gt;/...}).
*
* <p>Builds the protocol-specific request and executes it (mirrors {@link #createTemplateCache}).
* NFS uses file-clone in {@link UnifiedNASStrategy} instead.</p>
*
* <p><b>Combinations covered here:</b></p>
* <ul>
* <li>DATA or ROOT snapshot → new data volume on the same OntapiSCSI pool</li>
* <li>Same pool / FlexVol only (v1); cross-pool is not implemented</li>
* <li>In-place revert remains {@link #revertSnapshotForCloudStackVolume}; this method always
* creates a <em>new</em> LUN</li>
* <li>IOPS from snapshot_details are not applied here — see driver TODO</li>
* </ul>
*/
@Override
public CloudStackVolume cloneCloudStackVolumeFromSnapshot(StoragePoolVO storagePool, Map<String, String> details,
VolumeInfo volumeInfo, String sourceVolumePath,
String snapshotName) {
if (storagePool == null || details == null || volumeInfo == null) {
throw new CloudRuntimeException("Failed to clone Lun from snapshot, invalid request");
}
if (sourceVolumePath == null || sourceVolumePath.isEmpty()) {
throw new CloudRuntimeException("Failed to clone Lun from snapshot, source LUN path is required");
}
if (snapshotName == null || snapshotName.isEmpty()) {
throw new CloudRuntimeException("Failed to clone Lun from snapshot, snapshot name is required");
}

Lun lunRequest = buildCloneLunFromSnapshotRequest(storagePool, details, volumeInfo, sourceVolumePath, snapshotName);
logger.info("cloneCloudStackVolumeFromSnapshot [iSCSI]: Cloning LUN [{}] from snapshot source [{}]",
lunRequest.getName(), lunRequest.getClone().getSource().getName());
try {
String authHeader = OntapStorageUtils.generateAuthHeader(storage.getUsername(), storage.getPassword());
OntapResponse<Lun> clonedLun = sanFeignClient.createLun(authHeader, true, lunRequest);
if (clonedLun == null || CollectionUtils.isEmpty(clonedLun.getRecords())) {
logger.error("cloneCloudStackVolumeFromSnapshot: LUN clone returned no records for Lun {}",
lunRequest.getName());
throw new CloudRuntimeException("Failed to clone Lun from snapshot: " + lunRequest.getName());
}
Lun lun = clonedLun.getRecords().get(0);
validateCreatedLun(lun, lunRequest.getName(), "cloneCloudStackVolumeFromSnapshot");
logger.debug("cloneCloudStackVolumeFromSnapshot: LUN cloned successfully. Lun: {}", lun);

CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
clonedCloudStackVolume.setLun(lun);
return clonedCloudStackVolume;
} catch (FeignException e) {
logger.error("FeignException while cloning LUN from snapshot, Status: {}, Exception: {}",
e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to clone Lun from snapshot: " + e.getMessage());
} catch (CloudRuntimeException e) {
throw e;
} catch (Exception e) {
logger.error("Exception while cloning LUN from snapshot: {}", e.getMessage());
throw new CloudRuntimeException("Failed to clone Lun from snapshot: " + e.getMessage());
}
}

/**
* Builds {@code POST /api/storage/luns} clone request with snapshot-qualified source name
* {@code /vol/&lt;flexVol&gt;/.snapshot/&lt;snap&gt;/&lt;lun&gt;} (name required; uuid cannot
* identify a snapshot-resident LUN).
*/
private Lun buildCloneLunFromSnapshotRequest(StoragePoolVO storagePool, Map<String, String> details,
VolumeInfo volumeInfo, String sourceVolumePath,
String snapshotName) {
String lunName = volumeInfo.getName().replace(OntapStorageConstants.HYPHEN, OntapStorageConstants.UNDERSCORE);
if (!OntapStorageUtils.isValidName(lunName)) {
throw new CloudRuntimeException("Invalid dataObject name [" + lunName
+ "]. It must start with a letter and can only contain letters, digits, and underscores, and be up to 200 characters long.");
}

String flexVolName = details.get(OntapStorageConstants.VOLUME_NAME);
if (flexVolName == null || flexVolName.isEmpty()) {
flexVolName = storagePool.getName();
}
String snapshotSourceName = OntapStorageUtils.toLunCloneSourcePathInSnapshot(
sourceVolumePath, flexVolName, snapshotName);

Svm svm = new Svm();
svm.setName(details.get(OntapStorageConstants.SVM_NAME));

Lun.Source source = new Lun.Source();
source.setName(snapshotSourceName);
Lun.Clone clone = new Lun.Clone();
clone.setSource(source);

Lun lunRequest = new Lun();
lunRequest.setSvm(svm);
lunRequest.setName(OntapStorageUtils.getLunName(storagePool.getName(), lunName));
lunRequest.setClone(clone);
return lunRequest;
}

/**
* Ensures ONTAP returned a usable LUN identity from create/clone. Callers in the datastore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ public class CloudStackVolume {
*/
private String destinationPath;

/**
* ONTAP FlexVolume snapshot name when cloning a new file/LUN from a snapshot
* (create-volume-from-snapshot). Null for live clones (e.g. template cache).
*/
private String snapshotName;

private DataObject volumeInfo; // This is needed as we need DataObject to be passed to agent to create volume

public FileInfo getFile() {
Expand Down Expand Up @@ -101,4 +107,12 @@ public void setDestinationPath(String destinationPath) {
this.destinationPath = destinationPath;
}

public String getSnapshotName() {
return snapshotName;
}

public void setSnapshotName(String snapshotName) {
this.snapshotName = snapshotName;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ public class OntapStorageConstants {

public static final String VOLUME_PATH_PREFIX = "/vol/";

/**
* Path segment inserted after the FlexVol name when identifying a LUN inside a FlexVol snapshot
* for {@code POST /api/storage/luns} clone ({@code /vol/&lt;fv&gt;/.snapshot/&lt;snap&gt;/&lt;lun&gt;}).
*/
public static final String SNAPSHOT_PATH_SEGMENT = "/.snapshot/";

public static final String ONTAP_NAME_REGEX = "^[a-zA-Z][a-zA-Z0-9_]*$";
public static final String KVM = "KVM";

Expand All @@ -115,6 +121,13 @@ public class OntapStorageConstants {
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";
/**
* Optional {@code snapshot_details} keys: min/max IOPS from the source volume at take-snapshot
* time. Persisted only when the volume has configured values; applied to volumes created from
* the snapshot in a later change (see TODO on create-from-snapshot).
*/
public static final String MIN_IOPS = "min_iops";
public static final String MAX_IOPS = "max_iops";
public static final String FILE_PATH = "file_path";
public static final int MAX_SNAPSHOT_NAME_LENGTH = 255;
public static final String ONTAP_TEMP_CG_PREFIX = "cs-temp-cg-";
Expand Down Expand Up @@ -148,6 +161,14 @@ public class OntapStorageConstants {
*/
public static final String CLONE_OF_TEMPLATE = "cloneOfTemplate";

/**
* Key of the {@code volume_details} row that {@code StorageSystemDataMotionStrategy} writes
* immediately before {@code createAsync} when a volume is to be created from a CloudStack
* snapshot already present on this pool. The value is the CloudStack snapshot id. The literal
* must stay in sync with the string used by the orchestrator.
*/
public static final String CLONE_OF_SNAPSHOT = "cloneOfSnapshot";

// ASUP (AutoSupport) / EMS telemetry
public static final String ADVANCED_CONFIG_KEY_CATEGORY = "Advanced";
public static final String ASUP_CATEGORY = "provisioning";
Expand Down
Loading
Loading