Skip to content

Large diffs are not rendered by default.

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.FileCloneRequest;
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 Down Expand Up @@ -58,6 +60,15 @@ void createFile(@Param("authHeader") String authHeader,
@Param("path") String filePath,
FileInfo file);

/**
* Creates a space-efficient clone of a file within a FlexVolume.
*
* <p>ONTAP REST: {@code POST /api/storage/file/clone}</p>
*/
@RequestLine("POST /api/storage/file/clone")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
JobResponse cloneFile(@Param("authHeader") String authHeader, FileCloneRequest request);

// 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
Expand Up @@ -50,8 +50,8 @@ public interface SANFeignClient {
@Headers({"Authorization: {authHeader}"})
Lun getLunByUUID(@Param("authHeader") String authHeader, @Param("uuid") String uuid);

@RequestLine("PATCH /{uuid}")
@Headers({"Authorization: {authHeader}"})
@RequestLine("PATCH /api/storage/luns/{uuid}")
@Headers({"Authorization: {authHeader}", "Content-Type: application/json"})
void updateLun(@Param("authHeader") String authHeader, @Param("uuid") String uuid, Lun lun);

@RequestLine("DELETE /api/storage/luns/{uuid}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* 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.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Request body for the ONTAP file clone API.
*
* <p>ONTAP REST endpoint: {@code POST /api/storage/file/clone}</p>
*
* <p>Creates a space-efficient copy of a file. Source and destination paths are relative to the
* root of {@code volume}, and both must live in that same FlexVolume.</p>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FileCloneRequest {

@JsonProperty("volume")
private VolumeRef volume;

@JsonProperty("source_path")
private String sourcePath;

@JsonProperty("destination_path")
private String destinationPath;

@JsonProperty("overwrite_destination")
private Boolean overwriteDestination;

public FileCloneRequest() {
}

public FileCloneRequest(String flexVolUuid, String flexVolName, String sourcePath, String destinationPath) {
this.volume = new VolumeRef(flexVolUuid, flexVolName);
this.sourcePath = sourcePath;
this.destinationPath = destinationPath;
}

public VolumeRef getVolume() {
return volume;
}

public void setVolume(VolumeRef volume) {
this.volume = volume;
}

public String getSourcePath() {
return sourcePath;
}

public void setSourcePath(String sourcePath) {
this.sourcePath = sourcePath;
}

public String getDestinationPath() {
return destinationPath;
}

public void setDestinationPath(String destinationPath) {
this.destinationPath = destinationPath;
}

public Boolean getOverwriteDestination() {
return overwriteDestination;
}

public void setOverwriteDestination(Boolean overwriteDestination) {
this.overwriteDestination = overwriteDestination;
}

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class VolumeRef {

@JsonProperty("uuid")
private String uuid;

@JsonProperty("name")
private String name;

public VolumeRef() {
}

public VolumeRef(String uuid, String name) {
this.uuid = uuid;
this.name = name;
}

public String getUuid() {
return uuid;
}

public void setUuid(String uuid) {
this.uuid = uuid;
}

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 + "}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,7 @@ private String toIndentedString(Object o) {
}


@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Clone {
@JsonProperty("source")
private Source source = null;
Expand All @@ -319,6 +320,7 @@ public void setSource(Source source) {
}
}

@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Source {
@JsonProperty("name")
private String name = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -630,14 +630,28 @@ private boolean isIPv4Address(String address) {
abstract public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume);

/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
* Creates a space-efficient clone of an existing object inside the same FlexVolume.
* it is going to mimic
* cloneLun for iSCSI, FC protocols
* cloneFile for NFS3.0 and NFS4.1 protocols
* cloneNameSpace for Nvme/TCP and Nvme/FC protocol
* @param cloudstackVolume the CloudStack volume to copy
*
* <p>ONTAP requires the source and the destination to live in the same FlexVolume, which
* holds because a CloudStack primary storage pool maps one-to-one onto a FlexVolume.</p>
*
* @param cloudstackVolume describes the clone to create; the source is carried in the
* protocol-specific clone reference (for SAN, {@code lun.clone.source})
* @return the created CloudStackVolume, populated with the backend identity of the clone
*/
abstract public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume);

/**
* Grows an existing backend object to {@code sizeInBytes}.
*
* <p>Needed after cloning a cached template, because a clone inherits the size of its source
* while the service offering may ask for a larger disk.</p>
*/
abstract public void copyCloudStackVolume(CloudStackVolume cloudstackVolume);
abstract public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes);

/**
* Method encapsulates the behavior based on the opted protocol in subclasses.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
import org.apache.cloudstack.engine.subsystem.api.storage.EndPointSelector;
import org.apache.cloudstack.storage.command.CreateObjectCommand;
import org.apache.cloudstack.storage.command.DeleteCommand;
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
import org.apache.cloudstack.storage.feign.model.CliSnapshotRestoreRequest;
import org.apache.cloudstack.storage.feign.model.ExportPolicy;
import org.apache.cloudstack.storage.feign.model.ExportRule;
import org.apache.cloudstack.storage.feign.model.FileCloneRequest;
import org.apache.cloudstack.storage.feign.model.FileInfo;
import org.apache.cloudstack.storage.feign.model.Job;
import org.apache.cloudstack.storage.feign.model.Nas;
Expand All @@ -53,6 +56,8 @@
import org.apache.logging.log4j.Logger;

import com.cloud.agent.api.Answer;
import com.cloud.agent.api.storage.ResizeVolumeCommand;
import com.cloud.agent.api.to.StorageFilerTO;
import com.cloud.host.HostVO;
import com.cloud.storage.Storage;
import com.cloud.storage.VolumeVO;
Expand All @@ -66,6 +71,7 @@ public class UnifiedNASStrategy extends NASStrategy {
@Inject private VolumeDao volumeDao;
@Inject private EndPointSelector epSelector;
@Inject private StoragePoolDetailsDao storagePoolDetailsDao;
@Inject private PrimaryDataStoreDao primaryDataStoreDao;

public UnifiedNASStrategy(OntapStorage ontapStorage) {
super(ontapStorage);
Expand Down Expand Up @@ -117,9 +123,103 @@ public void deleteCloudStackVolume(CloudStackVolume cloudstackVolume) {
}
}

/**
* Clones a file inside the FlexVolume using ONTAP's file clone API.
*
* <p>The source is taken from {@code file.path} and the destination from
* {@code destinationPath}, both relative to the root of the FlexVolume backing the pool.</p>
*/
@Override
public CloudStackVolume cloneCloudStackVolume(CloudStackVolume cloudstackVolume) {
if (cloudstackVolume == null || cloudstackVolume.getFile() == null
|| cloudstackVolume.getFile().getPath() == null || cloudstackVolume.getDestinationPath() == null) {
logger.error("cloneCloudStackVolume: File clone failed. Invalid request: {}", cloudstackVolume);
throw new CloudRuntimeException("Failed to clone file, invalid request");
}
if (cloudstackVolume.getDatastoreId() == null) {
throw new CloudRuntimeException("Failed to clone file, no datastore id in the request");
}

Map<String, String> details = storagePoolDetailsDao.listDetailsKeyPairs(Long.parseLong(cloudstackVolume.getDatastoreId()));
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");
}
String sourcePath = cloudstackVolume.getFile().getPath();
String destinationPath = cloudstackVolume.getDestinationPath();

logger.info("cloneCloudStackVolume: Cloning file [{}] to [{}] in FlexVol [{}]", sourcePath, destinationPath, flexVolName);
try {
FileCloneRequest request = new FileCloneRequest(flexVolUuid, flexVolName, sourcePath, destinationPath);
JobResponse jobResponse = nasFeignClient.cloneFile(getAuthHeader(), request);
pollJobIfPresent(jobResponse, "clone file [" + sourcePath + "] to [" + destinationPath + "]");

updateCloudStackVolumeMetadata(cloudstackVolume.getDatastoreId(), cloudstackVolume.getVolumeInfo());

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

CloudStackVolume clonedCloudStackVolume = new CloudStackVolume();
clonedCloudStackVolume.setFile(clonedFile);
clonedCloudStackVolume.setDatastoreId(cloudstackVolume.getDatastoreId());
clonedCloudStackVolume.setVolumeInfo(cloudstackVolume.getVolumeInfo());
return clonedCloudStackVolume;
} catch (FeignException e) {
logger.error("FeignException occurred while cloning file [{}], Status: {}, Exception: {}",
sourcePath, e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to clone file: " + e.getMessage());
} catch (Exception e) {
logger.error("Exception occurred while cloning file [{}], Exception: {}", sourcePath, e.getMessage());
throw new CloudRuntimeException("Failed to clone file: " + e.getMessage());
}
}

/**
* Grows the cloned qcow2 to the requested size via a host-side {@code qemu-img resize}.
*/
@Override
public void copyCloudStackVolume(CloudStackVolume cloudstackVolume) {
public void resizeCloudStackVolume(CloudStackVolume cloudstackVolume, long sizeInBytes) {
if (cloudstackVolume == null || cloudstackVolume.getVolumeInfo() == null) {
logger.error("resizeCloudStackVolume: Resize failed. Invalid request: {}", cloudstackVolume);
throw new CloudRuntimeException("Failed to resize file, invalid request");
}
if (sizeInBytes <= 0) {
throw new CloudRuntimeException("Failed to resize file, invalid size " + sizeInBytes);
}

DataObject volumeInfo = cloudstackVolume.getVolumeInfo();
Answer answer = resizeVolumeOnKVMHost(volumeInfo, sizeInBytes);
Comment thread
rajiv-jain-netapp marked this conversation as resolved.
if (answer == null || !answer.getResult()) {
String errMsg = answer != null ? answer.getDetails() : "Failed to resize qcow2 on KVM host";
logger.error("resizeCloudStackVolume: " + errMsg);
throw new CloudRuntimeException(errMsg);
}
logger.info("resizeCloudStackVolume: Resized volume [{}] to {} bytes", volumeInfo.getUuid(), sizeInBytes);
}

private Answer resizeVolumeOnKVMHost(DataObject volumeInfo, long sizeInBytes) {
VolumeObject volumeObject = (VolumeObject) volumeInfo;
VolumeVO volume = volumeDao.findById(volumeObject.getId());
if (volume == null) {
throw new CloudRuntimeException("Volume not found with id: " + volumeObject.getId());
}

StoragePoolVO storagePool = primaryDataStoreDao.findById(volume.getPoolId());
if (storagePool == null) {
throw new CloudRuntimeException("Storage Pool not found for id: " + volume.getPoolId());
}

ResizeVolumeCommand cmd = new ResizeVolumeCommand(volume.getPath(), new StorageFilerTO(storagePool),
volume.getSize(), sizeInBytes, false, null);
EndPoint ep = epSelector.select(volumeInfo);
if (ep == null) {
String errMsg = "No remote endpoint to send ResizeVolumeCommand, check if host is up";
logger.error(errMsg);
return new Answer(cmd, false, errMsg);
}
logger.info("resizeVolumeOnKVMHost: Sending command to endpoint: {}", ep.getHostAddr());
return ep.sendMessage(cmd);
}

@Override
Expand Down Expand Up @@ -544,6 +644,28 @@ private Answer deleteVolumeOnKVMHost(DataObject volumeInfo) {
}
}

/**
* Deletes a file from a FlexVolume, treating an already-absent file as success.
*/
public void deleteFileByPath(String flexVolUuid, String filePath) {
logger.info("deleteFileByPath: Deleting file [{}] from FlexVol [{}]", filePath, flexVolUuid);
try {
nasFeignClient.deleteFile(getAuthHeader(), flexVolUuid, filePath);
logger.debug("deleteFileByPath: Deleted file [{}]", filePath);
} catch (FeignException e) {
if (e.status() == 404) {
logger.warn("deleteFileByPath: File [{}] does not exist (status 404), skipping deletion", filePath);
return;
}
logger.error("FeignException occurred while deleting file [{}], Status: {}, Exception: {}",
filePath, e.status(), e.getMessage());
throw new CloudRuntimeException("Failed to delete file: " + e.getMessage());
} catch (Exception e) {
logger.error("Exception occurred while deleting file [{}], Exception: {}", filePath, e.getMessage());
throw new CloudRuntimeException("Failed to delete file: " + e.getMessage());
}
}

private FileInfo getFile(String volumeUuid, String filePath) {
logger.info("Get File: {} for volume: {}", filePath, volumeUuid);

Expand Down
Loading
Loading