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 @@ -1007,16 +1007,19 @@ private TemplateInfo createManagedTemplateVolume(TemplateInfo srcTemplateInfo, P
} else {
errMesg = callback.result.getResult();
}
templateOnPrimary.processEvent(Event.OperationFailed);
Comment thread
rajiv-jain-netapp marked this conversation as resolved.
throw new CloudRuntimeException(String.format("Unable to create template %s on primary storage %s: %s", templateOnPrimary.getImage(), destPrimaryDataStore, errMesg));
}

templateOnPrimary.processEvent(Event.OperationSucceeded);

} catch (Throwable e) {
logger.debug("Failed to create template volume on storage", e);
templateOnPrimary.processEvent(Event.OperationFailed);
throw new CloudRuntimeException(e.getMessage());
try {
templateOnPrimary.processEvent(Event.OperationFailed);
} catch (Exception stateEx) {
logger.warn("Unable to mark template {} as failed on primary storage {}: {}", templateOnPrimary.getImage(), destPrimaryDataStore, stateEx.getMessage());
}
throw new CloudRuntimeException(e.getMessage(), e);
} finally {
_tmpltPoolDao.releaseFromLockTable(templatePoolRefId);
}
Expand Down

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. Creating the volume on a
* different pool 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 poolDetails 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> poolDetails,
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 @@ -157,7 +158,7 @@ public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume)
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, FlexVolume uuid is missing from pool details");
throw new CloudRuntimeException("Failed to clone file, FlexVolume uuid is missing from pool poolDetails");
}
String sourcePath = cloudstackVolume.getFile().getPath();
String destinationPath = cloudstackVolume.getDestinationPath();
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> poolDetails,
VolumeInfo volumeInfo, String sourceVolumePath,
String snapshotName) {
if (storagePool == null || poolDetails == 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 = poolDetails.get(OntapStorageConstants.VOLUME_UUID);
String flexVolName = poolDetails.get(OntapStorageConstants.VOLUME_NAME);
if (flexVolUuid == null || flexVolUuid.isEmpty()) {
throw new CloudRuntimeException("Failed to clone file from snapshot, FlexVolume uuid is missing from pool poolDetails");
}

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);
Comment thread
rajiv-jain-netapp marked this conversation as resolved.
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 Expand Up @@ -266,7 +337,7 @@ public AccessGroup createAccessGroup(AccessGroup accessGroup) {
logger.info("createAccessGroup: ExportPolicy created: {}, now attaching this policy to storage pool volume", createdPolicy.getName());
// attach export policy to volume of storage pool
assignExportPolicyToVolume(volumeUUID,createdPolicy.getName());
// save the export policy details in storage pool details
// save the export policy poolDetails in storage pool poolDetails
storagePoolDetailsDao.addDetail(accessGroup.getStoragePoolId(), OntapStorageConstants.EXPORT_POLICY_ID, String.valueOf(createdPolicy.getId()), true);
storagePoolDetailsDao.addDetail(accessGroup.getStoragePoolId(), OntapStorageConstants.EXPORT_POLICY_NAME, createdPolicy.getName(), true);
logger.info("Successfully assigned exportPolicy {} to volume {}", policyRequest.getName(), volumeName);
Expand Down Expand Up @@ -327,7 +398,7 @@ public AccessGroup updateAccessGroup(AccessGroup accessGroup) {

Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(accessGroup.getStoragePoolId());
if (details == null || details.isEmpty()) {
throw new CloudRuntimeException("No storage pool details found for storagePoolId: " + accessGroup.getStoragePoolId());
throw new CloudRuntimeException("No storage pool poolDetails found for storagePoolId: " + accessGroup.getStoragePoolId());
}
String exportPolicyId = details.get(OntapStorageConstants.EXPORT_POLICY_ID);
if (exportPolicyId == null || exportPolicyId.isEmpty()) {
Expand Down
Loading
Loading