Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -503,8 +503,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 @@ -29,6 +29,8 @@
import com.cloud.storage.Volume;
import com.cloud.storage.VolumeVO;
import com.cloud.storage.ScopeType;
import com.cloud.storage.dao.SnapshotDetailsDao;
import com.cloud.storage.dao.SnapshotDetailsVO;
import com.cloud.storage.dao.VolumeDao;
import com.cloud.storage.dao.VolumeDetailsDao;
import com.cloud.utils.Pair;
Expand All @@ -47,18 +49,22 @@
import org.apache.cloudstack.engine.subsystem.api.storage.VolumeInfo;
import org.apache.cloudstack.framework.async.AsyncCompletionCallback;
import org.apache.cloudstack.storage.command.CommandResult;
import org.apache.cloudstack.storage.command.CreateObjectAnswer;
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.FileInfo;
import org.apache.cloudstack.storage.feign.model.Lun;
import org.apache.cloudstack.storage.service.SANStrategy;
import org.apache.cloudstack.storage.service.StorageStrategy;
import org.apache.cloudstack.storage.service.UnifiedSANStrategy;
import org.apache.cloudstack.storage.service.model.AccessGroup;
import org.apache.cloudstack.storage.service.model.CloudStackVolume;
import org.apache.cloudstack.storage.service.model.ProtocolType;
import org.apache.cloudstack.storage.to.SnapshotObjectTO;
import org.apache.cloudstack.storage.utils.Constants;
import org.apache.cloudstack.storage.utils.Utility;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand All @@ -80,14 +86,16 @@ public class OntapPrimaryDatastoreDriver implements PrimaryDataStoreDriver {
@Inject private VMInstanceDao vmDao;
@Inject private VolumeDao volumeDao;
@Inject private VolumeDetailsDao volumeDetailsDao;
@Inject private SnapshotDetailsDao snapshotDetailsDao;

@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
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.FALSE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.FALSE.toString());
mapCapabilities.put(DataStoreCapabilities.STORAGE_SYSTEM_SNAPSHOT.toString(), Boolean.TRUE.toString());
mapCapabilities.put(DataStoreCapabilities.CAN_CREATE_VOLUME_FROM_SNAPSHOT.toString(), Boolean.TRUE.toString());
Copy link
Collaborator

Choose a reason for hiding this comment

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

We are supporting volume creation from snapshot with this PR?

Choose a reason for hiding this comment

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

Why do we need to set this as true as we are currently not supporting volume creation from snapshot.

Copy link
Author

Choose a reason for hiding this comment

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

Without this property set to true, CloudStack initiates both the snapshot creation and the corresponding copy operation to secondary storage over the data path, as it assumes that snapshots must reside on secondary storage by default.
When only the storage-system-snapshot property is enabled, CloudStack successfully invokes the takeSnapshot() method in the plugin driver class. However, the snapshot remains on the primary storage pool and is not transferred to secondary storage.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So, with this implementation, snapshots reside in primary storage itself?

return mapCapabilities;
}

Expand Down Expand Up @@ -524,7 +532,85 @@ public long getUsedIops(StoragePool storagePool) {

@Override
public void takeSnapshot(SnapshotInfo snapshot, AsyncCompletionCallback<CreateCmdResult> callback) {
s_logger.info("takeSnapshot : entered with snapshot id: " + snapshot.getId() + " and name: " + snapshot.getName());
Copy link
Collaborator

Choose a reason for hiding this comment

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

Lets have an intuitive log message instead of a method entry logger?

Copy link
Author

Choose a reason for hiding this comment

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

Could you provide a few examples?
Since I am also logging the method arguments, I felt that this approach would give me better insight into the method invocation flow, rather than relying solely on static logging.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Instead of adding an entry log we can add a log post line 544. For example, it could be like
s_logger.info("takeSnapshot: Snapshot with ID: " + snapshot.getId() + " and name: " + snapshot.getName() + " requested for volume with ID: " + volumeVO.getId() + " and name: " + volumeVO.getName())
It's just an example but I feel this could give us more outlook on whats happening.

CreateCmdResult result;

try {
VolumeInfo volumeInfo = snapshot.getBaseVolume();

VolumeVO volumeVO = volumeDao.findById(volumeInfo.getId());
if(volumeVO == null) {
throw new CloudRuntimeException("takeSnapshot: VolumeVO not found for id: " + volumeInfo.getId());
}

/** we are keeping file path at volumeVO.getPath() */

StoragePoolVO storagePool = storagePoolDao.findById(volumeVO.getPoolId());
if(storagePool == null) {
s_logger.error("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId());
throw new CloudRuntimeException("takeSnapshot : Storage Pool not found for id: " + volumeVO.getPoolId());
}
Map<String, String> poolDetails = storagePoolDetailsDao.listDetailsKeyPairs(volumeVO.getPoolId());
StorageStrategy storageStrategy = Utility.getStrategyByStoragePoolDetails(poolDetails);

CloudStackVolume cloudStackVolume = null;
long usedBytes = getUsedBytes(storagePool);
long capacityBytes = storagePool.getCapacityBytes();
long fileSize = 0l;
// Only proceed for NFS3 protocol
if (ProtocolType.NFS3.name().equalsIgnoreCase(poolDetails.get(Constants.PROTOCOL))) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Didn't we decide not to have any conditional statements on protocols?

Copy link
Author

Choose a reason for hiding this comment

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

We did, but the current context is not related to the hierarchy of strategy classes. The implementation here is analogous to the request‑preparation helper methods we already maintain within the driver classes.

Copy link
Collaborator

Choose a reason for hiding this comment

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

A part of the discussion was to have nothing related to protocols here, move all the necessary objects to strategy classes and have the necessary implementation there itself, according to the requirement. Will try to pick up the code for iSCSI soon, but, please take the lead in making code changes to align with the discussed approach?

Map<String, String> cloudStackVolumeRequestMap = new HashMap<>();
cloudStackVolumeRequestMap.put(Constants.VOLUME_UUID, poolDetails.get(Constants.VOLUME_UUID));
cloudStackVolumeRequestMap.put(Constants.FILE_PATH, volumeVO.getPath());
cloudStackVolume = storageStrategy.getCloudStackVolume(cloudStackVolumeRequestMap);
if (cloudStackVolume == null || cloudStackVolume.getFile() == null) {
throw new CloudRuntimeException("Failed to get source file to take snapshot");
}
s_logger.info("takeSnapshot : entered after getting cloudstack volume with file path: " + cloudStackVolume.getFile().getPath() + " and size: " + cloudStackVolume.getFile().getSize());
fileSize = cloudStackVolume.getFile().getSize();
usedBytes += fileSize;
}

Choose a reason for hiding this comment

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

snapshot or clone are not of the same size as actual volume(Until spllit clone). So , indirectly we are reducing the available space of storage pool for a clone/snapshot . We can query fr snapshot or clone size and update accordingly.


if (usedBytes > capacityBytes) {
throw new CloudRuntimeException("Insufficient space remains in this primary storage to take a snapshot");
}

storagePool.setUsedBytes(usedBytes);

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

String snapshotName = volumeInfo.getName() + "-" + snapshot.getUuid();

int trimRequired = snapshotName.length() - Constants.MAX_SNAPSHOT_NAME_LENGTH;

if (trimRequired > 0) {
snapshotName = StringUtils.left(volumeInfo.getName(), (volumeInfo.getName().length() - trimRequired)) + "-" + snapshot.getUuid();
}

CloudStackVolume snapCloudStackVolumeRequest = snapshotCloudStackVolumeRequestByProtocol(poolDetails, volumeVO.getPath(), snapshotName);
Copy link
Collaborator

Choose a reason for hiding this comment

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

As per our recent discussion, maybe all of this code has to be offloaded to protocol specific strategy class

Copy link
Author

Choose a reason for hiding this comment

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

we already have it below while calling actual snapshotCloudStackVolume() method. Request preparation we kept here for all the strategy calls.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Part of the discussion was to not create any request calls in driver class, push everything necessary to strategy classes and create the request necessary in the respective strategy class itself. BTW, the AI is on me to move the iSCSI specific request creation also into the respective strategy class. Will plan to pick it up soon but see if you can start this effort with snapshots?

CloudStackVolume cloneCloudStackVolume = storageStrategy.snapshotCloudStackVolume(snapCloudStackVolumeRequest);

updateSnapshotDetails(snapshot.getId(), volumeInfo.getId(), poolDetails.get(Constants.VOLUME_UUID), cloneCloudStackVolume.getFile().getPath(), volumeVO.getPoolId(), fileSize);
Copy link
Collaborator

Choose a reason for hiding this comment

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

If iSCSI needs different things to be updated in DB, we might get an if-else on protocol here also.


snapshotObjectTo.setPath(Constants.ONTAP_SNAP_ID +"="+cloneCloudStackVolume.getFile().getPath());

Choose a reason for hiding this comment

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

Is "=" intentional ?


/** Update size for the storage-pool including snapshot size */
storagePoolDao.update(volumeVO.getPoolId(), storagePool);

CreateObjectAnswer createObjectAnswer = new CreateObjectAnswer(snapshotObjectTo);

result = new CreateCmdResult(null, createObjectAnswer);

result.setResult(null);
}
catch (Exception ex) {
s_logger.error("takeSnapshot: Failed due to ", ex);
result = new CreateCmdResult(null, new CreateObjectAnswer(ex.toString()));

result.setResult(ex.toString());
}

callback.complete(result);
}

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

}

private CloudStackVolume snapshotCloudStackVolumeRequestByProtocol(Map<String, String> details,

Choose a reason for hiding this comment

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

this method is not used anywhere

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 calling this method in takeSnapshot()

String sourcePath,
String destinationPath) {
CloudStackVolume cloudStackVolumeRequest = null;
ProtocolType protocolType = null;
String protocol = null;

try {
protocol = details.get(Constants.PROTOCOL);
protocolType = ProtocolType.valueOf(protocol);
} catch (IllegalArgumentException e) {
throw new CloudRuntimeException("getCloudStackVolumeRequestByProtocol: Protocol: "+ protocol +" is not valid");
}
switch (protocolType) {
case NFS3:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned above, this was addressed elaborately in our discussion.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please take a relook? This was also planned to be pushed onto the strategy classes to be dealt with according to the requirement

cloudStackVolumeRequest = new CloudStackVolume();
FileInfo fileInfo = new FileInfo();
fileInfo.setPath(sourcePath);
cloudStackVolumeRequest.setFile(fileInfo);
String volumeUuid = details.get(Constants.VOLUME_UUID);
cloudStackVolumeRequest.setFlexVolumeUuid(volumeUuid);
cloudStackVolumeRequest.setDestinationPath(destinationPath);
break;
default:
throw new CloudRuntimeException("createCloudStackVolumeRequestByProtocol: Unsupported protocol " + protocol);

}
return cloudStackVolumeRequest;
}

/**
*
* @param csSnapshotId: generated snapshot id from cloudstack
* @param csVolumeId: Source CS volume id
* @param ontapVolumeUuid: storage flexvolume id
* @param ontapNewSnapshot: generated snapshot id from ONTAP
* @param storagePoolId: primary storage pool id
* @param ontapSnapSize: Size of snapshot CS volume(LUN/file)
*/
private void updateSnapshotDetails(long csSnapshotId, long csVolumeId, String ontapVolumeUuid, String ontapNewSnapshot, long storagePoolId, long ontapSnapSize) {

Choose a reason for hiding this comment

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

can we create a map and put whatever we need to at one time and persist in SnapshotDetailsV0 ?

Copy link
Author

Choose a reason for hiding this comment

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

We store these details as key–value pairs in the database. To facilitate efficient querying—particularly during snapshot delete or revert operations—I prefer to keep them independent rather than embedding them within a combined structure.

SnapshotDetailsVO snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.SRC_CS_VOLUME_ID, String.valueOf(csVolumeId), false);
snapshotDetailsDao.persist(snapshotDetail);

snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.BASE_ONTAP_FV_ID, String.valueOf(ontapVolumeUuid), false);
snapshotDetailsDao.persist(snapshotDetail);

snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_ID, String.valueOf(ontapNewSnapshot), false);
snapshotDetailsDao.persist(snapshotDetail);

snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.PRIMARY_POOL_ID, String.valueOf(storagePoolId), false);
snapshotDetailsDao.persist(snapshotDetail);

snapshotDetail = new SnapshotDetailsVO(csSnapshotId, Constants.ONTAP_SNAP_SIZE, String.valueOf(ontapSnapSize), false);
snapshotDetailsDao.persist(snapshotDetail);
}

}
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,53 @@
/*
* 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;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class FileClone {
@JsonProperty("source_path")
private String sourcePath;
@JsonProperty("destination_path")
private String destinationPath;
@JsonProperty("volume")
private VolumeConcise volume;
public VolumeConcise getVolume() {
return volume;
}
public void setVolume(VolumeConcise 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;
}
}
Loading
Loading