Skip to content

Commit 1c549f9

Browse files
[CLOUDSTACK-10323] Allow changing disk offering during volume migration
This is a continuation of work developed on PR #2425 (CLOUDSTACK-10240), which provided root admins an override mechanism to move volumes between storage systems types (local/shared) even when the disk offering would not allow such operation. To complete the work, we will now provide a way for administrators to enter a new disk offering that can reflect the new placement of the volume. We will add an extra parameter to allow the root admin inform a new disk offering for the volume. Therefore, when the volume is being migrated, it will be possible to replace the disk offering to reflect the new placement of the volume. The API method will have the following parameters: * storageid (required) * volumeid (required) * livemigrate(optional) * newdiskofferingid (optional) – this is the new parameter The expected behavior is the following: * If “newdiskofferingid” is not provided the current behavior is maintained. Override mechanism will also keep working as we have seen so far. * If the “newdiskofferingid” is provided by the admin, we will execute the following checks ** new disk offering mode (local/shared) must match the target storage mode. If it does not match, an exception will be thrown and the operator will receive a message indicating the problem. ** we will check if the new disk offering tags match the target storage tags. If it does not match, an exception will be thrown and the operator will receive a message indicating the problem. ** check if the target storage has the capacity for the new volume. If it does not have enough space, then an exception is thrown and the operator will receive a message indicating the problem. ** check if the size of the volume is the same as the size of the new disk offering. If it is not the same, we will ALLOW the change of the service offering, and a warning message will be logged. We execute the change of the Disk offering as soon as the migration of the volume finishes. Therefore, if an error happens during the migration and the volume remains in the original storage system, the disk offering will keep reflecting this situation.
1 parent 8a3943b commit 1c549f9

27 files changed

Lines changed: 493 additions & 137 deletions

File tree

api/src/main/java/org/apache/cloudstack/api/ApiConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ public class ApiConstants {
8686
public static final String DEVICE_ID = "deviceid";
8787
public static final String DIRECT_DOWNLOAD = "directdownload";
8888
public static final String DISK_OFFERING_ID = "diskofferingid";
89+
public static final String NEW_DISK_OFFERING_ID = "newdiskofferingid";
8990
public static final String DISK_SIZE = "disksize";
9091
public static final String UTILIZATION = "utilization";
9192
public static final String DRIVER = "driver";

api/src/main/java/org/apache/cloudstack/api/command/admin/volume/MigrateVolumeCmdByAdmin.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@ public class MigrateVolumeCmdByAdmin extends MigrateVolumeCmd {
3333

3434
@Override
3535
public void execute(){
36-
Volume result;
37-
38-
result = _volumeService.migrateVolume(this);
36+
Volume result = _volumeService.migrateVolume(this);
3937
if (result != null) {
4038
VolumeResponse response = _responseGenerator.createVolumeResponse(ResponseView.Full, result);
4139
response.setResponseName(getCommandName());

api/src/main/java/org/apache/cloudstack/api/command/user/volume/MigrateVolumeCmd.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ public class MigrateVolumeCmd extends BaseAsyncCmd {
5555
description = "if the volume should be live migrated when it is attached to a running vm")
5656
private Boolean liveMigrate;
5757

58+
@Parameter(name = ApiConstants.NEW_DISK_OFFERING_ID, type = CommandType.STRING, description = "The new disk offering ID that replaces the current one used by the volume. This new disk offering is used to better reflect the new storage where the volume is going to be migrated to.")
59+
private String newDiskOfferingUuid;
60+
5861
/////////////////////////////////////////////////////
5962
/////////////////// Accessors ///////////////////////
6063
/////////////////////////////////////////////////////
@@ -105,6 +108,10 @@ public String getEventDescription() {
105108
return "Attempting to migrate volume Id: " + getVolumeId() + " to storage pool Id: " + getStoragePoolId();
106109
}
107110

111+
public String getNewDiskOfferingUuid() {
112+
return newDiskOfferingUuid;
113+
}
114+
108115
@Override
109116
public void execute() {
110117
Volume result;

engine/components-api/src/main/java/com/cloud/vm/VmWorkMigrateVolume.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ public class VmWorkMigrateVolume extends VmWork {
2222
private long volumeId;
2323
private long destPoolId;
2424
private boolean liveMigrate;
25+
private Long newDiskOfferingId;
2526

26-
public VmWorkMigrateVolume(long userId, long accountId, long vmId, String handlerName, long volumeId, long destPoolId, boolean liveMigrate) {
27+
public VmWorkMigrateVolume(long userId, long accountId, long vmId, String handlerName, long volumeId, long destPoolId, boolean liveMigrate, Long newDiskOfferingId) {
2728
super(userId, accountId, vmId, handlerName);
2829
this.volumeId = volumeId;
2930
this.destPoolId = destPoolId;
3031
this.liveMigrate = liveMigrate;
32+
this.newDiskOfferingId = newDiskOfferingId;
3133
}
3234

3335
public long getVolumeId() {
@@ -41,4 +43,8 @@ public long getDestPoolId() {
4143
public boolean isLiveMigrate() {
4244
return liveMigrate;
4345
}
46+
47+
public Long getNewDiskOfferingId() {
48+
return newDiskOfferingId;
49+
}
4450
}

engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,7 +986,9 @@ public void revokeAccess(long vmId, long hostId) {
986986
@DB
987987
public Volume migrateVolume(Volume volume, StoragePool destPool) throws StorageUnavailableException {
988988
VolumeInfo vol = volFactory.getVolume(volume.getId());
989-
AsyncCallFuture<VolumeApiResult> future = volService.copyVolume(vol, (DataStore)destPool);
989+
990+
DataStore dataStoreTarget = dataStoreMgr.getDataStore(destPool.getId(), DataStoreRole.Primary);
991+
AsyncCallFuture<VolumeApiResult> future = volService.copyVolume(vol, dataStoreTarget);
990992
try {
991993
VolumeApiResult result = future.get();
992994
if (result.isFailed()) {

engine/schema/src/main/java/com/cloud/storage/DiskOfferingVO.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,4 +514,8 @@ public void setHypervisorSnapshotReserve(Integer hypervisorSnapshotReserve) {
514514
public Integer getHypervisorSnapshotReserve() {
515515
return hypervisorSnapshotReserve;
516516
}
517+
518+
public boolean isShared() {
519+
return !useLocalStorage;
520+
}
517521
}

engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,9 @@ public interface VolumeDao extends GenericDao<VolumeVO, Long>, StateDao<Volume.S
122122
* @return returns true if transaction is successful.
123123
*/
124124
boolean updateUuid(long srcVolId, long destVolId);
125+
126+
/**
127+
* Updates the disk offering for the given volume.
128+
*/
129+
void updateDiskOffering(long volumeId, long diskOfferingId);
125130
}

engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,4 +685,17 @@ public ScopeType getVolumeStoragePoolScope(long volumeId) {
685685
}
686686
return null;
687687
}
688+
689+
private String sqlUpdateDiskOffering = "UPDATE volumes SET disk_offering_id = ? where id =?";
690+
public void updateDiskOffering(long volumeId, long diskOfferingId) {
691+
try (TransactionLegacy txn = TransactionLegacy.currentTxn();
692+
PreparedStatement pstmt = txn.prepareAutoCloseStatement(sqlUpdateDiskOffering)) {
693+
pstmt.setLong(1, diskOfferingId);
694+
pstmt.setLong(2, volumeId);
695+
pstmt.executeUpdate();
696+
txn.commit();
697+
} catch (SQLException e) {
698+
throw new CloudRuntimeException(e);
699+
}
700+
}
688701
}

server/src/main/java/com/cloud/storage/VolumeApiServiceImpl.java

Lines changed: 111 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,16 @@
6969
import org.apache.cloudstack.storage.command.DettachCommand;
7070
import org.apache.cloudstack.storage.command.TemplateOrVolumePostUploadCommand;
7171
import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao;
72+
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailVO;
73+
import org.apache.cloudstack.storage.datastore.db.StoragePoolDetailsDao;
7274
import org.apache.cloudstack.storage.datastore.db.StoragePoolVO;
7375
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreDao;
7476
import org.apache.cloudstack.storage.datastore.db.VolumeDataStoreVO;
7577
import org.apache.cloudstack.storage.image.datastore.ImageStoreEntity;
7678
import org.apache.cloudstack.utils.identity.ManagementServerNode;
7779
import org.apache.cloudstack.utils.imagestore.ImageStoreUtil;
7880
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
81+
import org.apache.commons.collections.CollectionUtils;
7982
import org.apache.log4j.Logger;
8083
import org.joda.time.DateTime;
8184
import org.joda.time.DateTimeZone;
@@ -247,6 +250,8 @@ public class VolumeApiServiceImpl extends ManagerBase implements VolumeApiServic
247250
private ClusterDetailsDao _clusterDetailsDao;
248251
@Inject
249252
private StorageManager storageMgr;
253+
@Inject
254+
private StoragePoolDetailsDao storagePoolDetailsDao;
250255

251256
protected Gson _gson;
252257

@@ -2052,7 +2057,8 @@ public Volume migrateVolume(MigrateVolumeCmd cmd) {
20522057
updateMissingRootDiskController(vm, vol.getChainInfo());
20532058
}
20542059
}
2055-
2060+
DiskOfferingVO newDiskOffering = retrieveAndValidateNewDiskOffering(cmd);
2061+
validateConditionsToReplaceDiskOfferingOfVolume(vol, newDiskOffering, destPool);
20562062
if (vm != null) {
20572063
// serialize VM operation
20582064
AsyncJobExecutionContext jobContext = AsyncJobExecutionContext.getCurrentExecutionContext();
@@ -2062,13 +2068,13 @@ public Volume migrateVolume(MigrateVolumeCmd cmd) {
20622068
VmWorkJobVO placeHolder = null;
20632069
placeHolder = createPlaceHolderWork(vm.getId());
20642070
try {
2065-
return orchestrateMigrateVolume(vol.getId(), destPool.getId(), liveMigrateVolume);
2071+
return orchestrateMigrateVolume(vol, destPool, liveMigrateVolume, newDiskOffering);
20662072
} finally {
20672073
_workJobDao.expunge(placeHolder.getId());
20682074
}
20692075

20702076
} else {
2071-
Outcome<Volume> outcome = migrateVolumeThroughJobQueue(vm.getId(), vol.getId(), destPool.getId(), liveMigrateVolume);
2077+
Outcome<Volume> outcome = migrateVolumeThroughJobQueue(vm, vol, destPool, liveMigrateVolume, newDiskOffering);
20722078

20732079
try {
20742080
outcome.get();
@@ -2097,21 +2103,97 @@ public Volume migrateVolume(MigrateVolumeCmd cmd) {
20972103
}
20982104
}
20992105

2100-
return orchestrateMigrateVolume(vol.getId(), destPool.getId(), liveMigrateVolume);
2106+
return orchestrateMigrateVolume(vol, destPool, liveMigrateVolume, newDiskOffering);
21012107
}
21022108

2103-
private Volume orchestrateMigrateVolume(long volumeId, long destPoolId, boolean liveMigrateVolume) {
2104-
VolumeVO vol = _volsDao.findById(volumeId);
2105-
assert (vol != null);
2106-
StoragePool destPool = (StoragePool)dataStoreMgr.getDataStore(destPoolId, DataStoreRole.Primary);
2107-
assert (destPool != null);
2109+
/**
2110+
* Retrieve the new disk offering UUID that might be sent to replace the current one in the volume being migrated.
2111+
* If no disk offering UUID is provided we return null. Otherwise, we perform the following checks.
2112+
* <ul>
2113+
* <li>Is the disk offering UUID entered valid? If not, an {@link InvalidParameterValueException} is thrown;
2114+
* <li>If the disk offering was already removed, we thrown an {@link InvalidParameterValueException} is thrown;
2115+
* <li>We then check if the user executing the operation has access to the given disk offering.
2116+
* </ul>
2117+
*
2118+
* If all checks pass, we move forward returning the disk offering object.
2119+
*/
2120+
private DiskOfferingVO retrieveAndValidateNewDiskOffering(MigrateVolumeCmd cmd) {
2121+
String newDiskOfferingUuid = cmd.getNewDiskOfferingUuid();
2122+
if (org.apache.commons.lang.StringUtils.isBlank(newDiskOfferingUuid)) {
2123+
return null;
2124+
}
2125+
DiskOfferingVO newDiskOffering = _diskOfferingDao.findByUuid(newDiskOfferingUuid);
2126+
if (newDiskOffering == null) {
2127+
throw new InvalidParameterValueException(String.format("The disk offering informed is not valid [id=%s].", newDiskOfferingUuid));
2128+
}
2129+
if (newDiskOffering.getRemoved() != null) {
2130+
throw new InvalidParameterValueException(String.format("We cannot assign a removed disk offering [id=%s] to a volume. ", newDiskOffering.getUuid()));
2131+
}
2132+
Account caller = CallContext.current().getCallingAccount();
2133+
_accountMgr.checkAccess(caller, newDiskOffering);
2134+
return newDiskOffering;
2135+
}
2136+
2137+
/**
2138+
* Performs the validations required for replacing the disk offering while migrating the volume of storage. If no new disk offering is provided, we do not execute any validation.
2139+
* If a disk offering is informed, we then proceed with the following checks.
2140+
* <ul>
2141+
* <li>We check if the given volume is of ROOT type. We cannot change the disk offering of a ROOT volume. Therefore, we thrown an {@link InvalidParameterValueException}.
2142+
* <li>We the disk is being migrated to shared storage and the new disk offering is for local storage (or vice versa), we throw an {@link InvalidParameterValueException}. Bear in mind that we are validating only the new disk offering. If none is provided we can override the current disk offering. This means, placing a volume with shared disk offering in local storage and vice versa.
2143+
* <li>We then proceed checking if the tags of the new disk offerings match the tags of the target storage. If they do not match an {@link InvalidParameterValueException} is thrown.
2144+
* </ul>
2145+
*
2146+
* If all of the above validations pass, we check if the size of the new disk offering is different from the volume. If it is, we log a warning message.
2147+
*/
2148+
protected void validateConditionsToReplaceDiskOfferingOfVolume(VolumeVO volume, DiskOfferingVO newDiskOffering, StoragePool destPool) {
2149+
if (newDiskOffering == null) {
2150+
return;
2151+
}
2152+
if (Volume.Type.ROOT.equals(volume.getVolumeType())) {
2153+
throw new InvalidParameterValueException(String.format("Cannot change the disk offering of a ROOT volume [id=%s].", volume.getUuid()));
2154+
}
2155+
if ((destPool.isShared() && newDiskOffering.getUseLocalStorage()) || destPool.isLocal() && newDiskOffering.isShared()) {
2156+
throw new InvalidParameterValueException("You cannot move the volume to a shared storage and assing a disk offering for local storage and vice versa.");
2157+
}
2158+
String storageTags = getStoragePoolTags(destPool);
2159+
if (!StringUtils.areTagsEqual(storageTags, newDiskOffering.getTags())) {
2160+
throw new InvalidParameterValueException(String.format("Target Storage [id=%s] tags [%s] does not match new disk offering [id=%s] tags [%s].", destPool.getUuid(), storageTags,
2161+
newDiskOffering.getUuid(), newDiskOffering.getTags()));
2162+
}
2163+
if (volume.getSize() != newDiskOffering.getDiskSize()) {
2164+
DiskOfferingVO oldDiskOffering = this._diskOfferingDao.findById(volume.getDiskOfferingId());
2165+
s_logger.warn(String.format(
2166+
"You are migrating a volume [id=%s] and changing the disk offering[from id=%s to id=%s] to reflect this migration. However, the sizes of the volume and the new disk offering are different.",
2167+
volume.getUuid(), oldDiskOffering.getUuid(), newDiskOffering.getUuid()));
2168+
}
21082169

2170+
}
2171+
2172+
/**
2173+
* Retrieve the storage pool tags as a {@link String}. If the storage pool does not have tags we return a null value.
2174+
*/
2175+
protected String getStoragePoolTags(StoragePool destPool) {
2176+
List<StoragePoolDetailVO> storagePoolDetails = storagePoolDetailsDao.listDetails(destPool.getId());
2177+
if (CollectionUtils.isEmpty(storagePoolDetails)) {
2178+
return null;
2179+
}
2180+
String storageTags = "";
2181+
for (StoragePoolDetailVO storagePoolDetailVO : storagePoolDetails) {
2182+
storageTags = storageTags + storagePoolDetailVO.getName() + ",";
2183+
}
2184+
return storageTags.substring(0, storageTags.length() - 1);
2185+
}
2186+
2187+
private Volume orchestrateMigrateVolume(VolumeVO volume, StoragePool destPool, boolean liveMigrateVolume, DiskOfferingVO newDiskOffering) {
21092188
Volume newVol = null;
21102189
try {
21112190
if (liveMigrateVolume) {
2112-
newVol = liveMigrateVolume(vol, destPool);
2191+
newVol = liveMigrateVolume(volume, destPool);
21132192
} else {
2114-
newVol = _volumeMgr.migrateVolume(vol, destPool);
2193+
newVol = _volumeMgr.migrateVolume(volume, destPool);
2194+
}
2195+
if (newDiskOffering != null) {
2196+
_volsDao.updateDiskOffering(newVol.getId(), newDiskOffering.getId());
21152197
}
21162198
} catch (StorageUnavailableException e) {
21172199
s_logger.debug("Failed to migrate volume", e);
@@ -2126,7 +2208,9 @@ private Volume orchestrateMigrateVolume(long volumeId, long destPoolId, boolean
21262208
@DB
21272209
protected Volume liveMigrateVolume(Volume volume, StoragePool destPool) throws StorageUnavailableException {
21282210
VolumeInfo vol = volFactory.getVolume(volume.getId());
2129-
AsyncCallFuture<VolumeApiResult> future = volService.migrateVolume(vol, (DataStore)destPool);
2211+
2212+
DataStore dataStoreTarget = dataStoreMgr.getDataStore(destPool.getId(), DataStoreRole.Primary);
2213+
AsyncCallFuture<VolumeApiResult> future = volService.migrateVolume(vol, dataStoreTarget);
21302214
try {
21312215
VolumeApiResult result = future.get();
21322216
if (result.isFailed()) {
@@ -3019,14 +3103,10 @@ public Outcome<String> extractVolumeThroughJobQueue(final Long vmId, final long
30193103
return new VmJobVolumeUrlOutcome(workJob);
30203104
}
30213105

3022-
public Outcome<Volume> migrateVolumeThroughJobQueue(final Long vmId, final long volumeId,
3023-
final long destPoolId, final boolean liveMigrate) {
3024-
3025-
final CallContext context = CallContext.current();
3026-
final User callingUser = context.getCallingUser();
3027-
final Account callingAccount = context.getCallingAccount();
3028-
3029-
final VMInstanceVO vm = _vmInstanceDao.findById(vmId);
3106+
private Outcome<Volume> migrateVolumeThroughJobQueue(VMInstanceVO vm, VolumeVO vol, StoragePool destPool, boolean liveMigrateVolume, DiskOfferingVO newDiskOffering) {
3107+
CallContext context = CallContext.current();
3108+
User callingUser = context.getCallingUser();
3109+
Account callingAccount = context.getCallingAccount();
30303110

30313111
VmWorkJobVO workJob = new VmWorkJobVO(context.getContextId());
30323112

@@ -3040,16 +3120,18 @@ public Outcome<Volume> migrateVolumeThroughJobQueue(final Long vmId, final long
30403120
workJob.setVmInstanceId(vm.getId());
30413121
workJob.setRelated(AsyncJobExecutionContext.getOriginJobId());
30423122

3123+
Long newDiskOfferingId = newDiskOffering != null ? newDiskOffering.getId() : null;
3124+
30433125
// save work context info (there are some duplications)
30443126
VmWorkMigrateVolume workInfo = new VmWorkMigrateVolume(callingUser.getId(), callingAccount.getId(), vm.getId(),
3045-
VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, volumeId, destPoolId, liveMigrate);
3127+
VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, vol.getId(), destPool.getId(), liveMigrateVolume, newDiskOfferingId);
30463128
workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo));
30473129

30483130
_jobMgr.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vm.getId());
30493131

30503132
AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId());
30513133

3052-
return new VmJobVolumeOutcome(workJob,volumeId);
3134+
return new VmJobVolumeOutcome(workJob, vol.getId());
30533135
}
30543136

30553137
public Outcome<Snapshot> takeVolumeSnapshotThroughJobQueue(final Long vmId, final Long volumeId,
@@ -3117,9 +3199,13 @@ private Pair<JobInfo.Status, String> orchestrateResizeVolume(VmWorkResizeVolume
31173199

31183200
@ReflectionUse
31193201
private Pair<JobInfo.Status, String> orchestrateMigrateVolume(VmWorkMigrateVolume work) throws Exception {
3120-
Volume newVol = orchestrateMigrateVolume(work.getVolumeId(), work.getDestPoolId(), work.isLiveMigrate());
3121-
return new Pair<JobInfo.Status, String>(JobInfo.Status.SUCCEEDED,
3122-
_jobMgr.marshallResultObject(new Long(newVol.getId())));
3202+
VolumeVO volume = _volsDao.findById(work.getVolumeId());
3203+
StoragePoolVO targetStoragePool = _storagePoolDao.findById(work.getDestPoolId());
3204+
DiskOfferingVO newDiskOffering = _diskOfferingDao.findById(work.getNewDiskOfferingId());
3205+
3206+
Volume newVol = orchestrateMigrateVolume(volume, targetStoragePool, work.isLiveMigrate(), newDiskOffering);
3207+
3208+
return new Pair<JobInfo.Status, String>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(newVol.getId()));
31233209
}
31243210

31253211
@ReflectionUse

0 commit comments

Comments
 (0)