Skip to content

Commit 238f7cb

Browse files
[CLOUDSTACK-10323] Allow changing disk offering during volume migration
This is a continuation of work developed on PR apache#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 c3488a5 commit 238f7cb

File tree

26 files changed

+341
-134
lines changed

26 files changed

+341
-134
lines changed

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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,4 +122,6 @@ 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+
void updateDiskOffering(long volumeId, long diskOfferingId);
125127
}

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: 85 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+
validateNewDiskOffering(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,71 @@ public Volume migrateVolume(MigrateVolumeCmd cmd) {
20972103
}
20982104
}
20992105

2100-
return orchestrateMigrateVolume(vol.getId(), destPool.getId(), liveMigrateVolume);
2106+
return orchestrateMigrateVolume(vol, destPool, liveMigrateVolume, newDiskOffering);
21012107
}
2108+
2109+
private DiskOfferingVO retrieveAndValidateNewDiskOffering(MigrateVolumeCmd cmd) {
2110+
String newDiskOfferingUuid = cmd.getNewDiskOfferingUuid();
2111+
if (org.apache.commons.lang.StringUtils.isBlank(newDiskOfferingUuid)) {
2112+
return null;
2113+
}
2114+
DiskOfferingVO newDiskOffering = _diskOfferingDao.findByUuid(newDiskOfferingUuid);
2115+
if (newDiskOffering == null) {
2116+
throw new InvalidParameterValueException(String.format("The disk offering informed is not valid [id=%s].", newDiskOfferingUuid));
2117+
}
2118+
if (newDiskOffering.getRemoved() != null) {
2119+
throw new InvalidParameterValueException(String.format("We cannot assign a removed disk offering [id=%s] to a volume. ", newDiskOffering.getUuid()));
2120+
}
2121+
Account caller = CallContext.current().getCallingAccount();
2122+
_accountMgr.checkAccess(caller, newDiskOffering);
2123+
return newDiskOffering;
2124+
}
21022125

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);
2126+
private void validateNewDiskOffering(VolumeVO vol, DiskOfferingVO newDiskOffering, StoragePool destPool) {
2127+
if (newDiskOffering == null) {
2128+
return;
2129+
}
2130+
if (Volume.Type.ROOT.equals(vol.getVolumeType())) {
2131+
throw new InvalidParameterValueException(String.format("Cannot change the disk offering of a ROOT volume [id=%s].", vol.getUuid()));
2132+
}
2133+
if ((destPool.isShared() && newDiskOffering.getUseLocalStorage()) || destPool.isLocal() && newDiskOffering.isShared()) {
2134+
throw new InvalidParameterValueException("You cannot move the volume to a shared storage and assing a disk offering for local storage and vice versa.");
2135+
}
2136+
String storageTags = getStoragePoolTags(destPool);
2137+
if (!StringUtils.areTagsEqual(storageTags, newDiskOffering.getTags())) {
2138+
throw new InvalidParameterValueException(String.format("Target Storage [id=%s] tags [%s] does not match new disk offering [id=%s] tags [%s].", destPool.getUuid(), storageTags,
2139+
newDiskOffering.getUuid(), newDiskOffering.getTags()));
2140+
}
2141+
if (vol.getSize() != newDiskOffering.getDiskSize()) {
2142+
DiskOfferingVO oldDiskOffering = this._diskOfferingDao.findById(vol.getDiskOfferingId());
2143+
s_logger.warn(String.format(
2144+
"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.",
2145+
vol.getUuid(), oldDiskOffering.getUuid(), newDiskOffering.getUuid()));
2146+
}
21082147

2148+
}
2149+
private String getStoragePoolTags(StoragePool destPool) {
2150+
List<StoragePoolDetailVO> storagePoolDetails = storagePoolDetailsDao.listDetails(destPool.getId());
2151+
if (CollectionUtils.isEmpty(storagePoolDetails)) {
2152+
return null;
2153+
}
2154+
String storageTags = "";
2155+
for (StoragePoolDetailVO storagePoolDetailVO : storagePoolDetails) {
2156+
storageTags = storageTags + storagePoolDetailVO.getName() + ",";
2157+
}
2158+
return storageTags.substring(0, storageTags.length() - 1);
2159+
}
2160+
2161+
private Volume orchestrateMigrateVolume(VolumeVO volume, StoragePool destPool, boolean liveMigrateVolume, DiskOfferingVO newDiskOffering) {
21092162
Volume newVol = null;
21102163
try {
21112164
if (liveMigrateVolume) {
2112-
newVol = liveMigrateVolume(vol, destPool);
2165+
newVol = liveMigrateVolume(volume, destPool);
21132166
} else {
2114-
newVol = _volumeMgr.migrateVolume(vol, destPool);
2167+
newVol = _volumeMgr.migrateVolume(volume, destPool);
2168+
}
2169+
if (newDiskOffering != null) {
2170+
_volsDao.updateDiskOffering(newVol.getId(), newDiskOffering.getId());
21152171
}
21162172
} catch (StorageUnavailableException e) {
21172173
s_logger.debug("Failed to migrate volume", e);
@@ -2126,7 +2182,9 @@ private Volume orchestrateMigrateVolume(long volumeId, long destPoolId, boolean
21262182
@DB
21272183
protected Volume liveMigrateVolume(Volume volume, StoragePool destPool) throws StorageUnavailableException {
21282184
VolumeInfo vol = volFactory.getVolume(volume.getId());
2129-
AsyncCallFuture<VolumeApiResult> future = volService.migrateVolume(vol, (DataStore)destPool);
2185+
2186+
DataStore dataStoreTarget = dataStoreMgr.getDataStore(destPool.getId(), DataStoreRole.Primary);
2187+
AsyncCallFuture<VolumeApiResult> future = volService.migrateVolume(vol, dataStoreTarget);
21302188
try {
21312189
VolumeApiResult result = future.get();
21322190
if (result.isFailed()) {
@@ -3019,14 +3077,10 @@ public Outcome<String> extractVolumeThroughJobQueue(final Long vmId, final long
30193077
return new VmJobVolumeUrlOutcome(workJob);
30203078
}
30213079

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);
3080+
private Outcome<Volume> migrateVolumeThroughJobQueue(VMInstanceVO vm, VolumeVO vol, StoragePool destPool, boolean liveMigrateVolume, DiskOfferingVO newDiskOffering) {
3081+
CallContext context = CallContext.current();
3082+
User callingUser = context.getCallingUser();
3083+
Account callingAccount = context.getCallingAccount();
30303084

30313085
VmWorkJobVO workJob = new VmWorkJobVO(context.getContextId());
30323086

@@ -3040,16 +3094,18 @@ public Outcome<Volume> migrateVolumeThroughJobQueue(final Long vmId, final long
30403094
workJob.setVmInstanceId(vm.getId());
30413095
workJob.setRelated(AsyncJobExecutionContext.getOriginJobId());
30423096

3097+
Long newDiskOfferingId = newDiskOffering != null ? newDiskOffering.getId() : null;
3098+
30433099
// save work context info (there are some duplications)
30443100
VmWorkMigrateVolume workInfo = new VmWorkMigrateVolume(callingUser.getId(), callingAccount.getId(), vm.getId(),
3045-
VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, volumeId, destPoolId, liveMigrate);
3101+
VolumeApiServiceImpl.VM_WORK_JOB_HANDLER, vol.getId(), destPool.getId(), liveMigrateVolume, newDiskOfferingId);
30463102
workJob.setCmdInfo(VmWorkSerializer.serialize(workInfo));
30473103

30483104
_jobMgr.submitAsyncJob(workJob, VmWorkConstants.VM_WORK_QUEUE, vm.getId());
30493105

30503106
AsyncJobExecutionContext.getCurrentExecutionContext().joinJob(workJob.getId());
30513107

3052-
return new VmJobVolumeOutcome(workJob,volumeId);
3108+
return new VmJobVolumeOutcome(workJob, vol.getId());
30533109
}
30543110

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

31183174
@ReflectionUse
31193175
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())));
3176+
VolumeVO volume = _volsDao.findById(work.getVolumeId());
3177+
StoragePoolVO targetStoragePool = _storagePoolDao.findById(work.getDestPoolId());
3178+
DiskOfferingVO newDiskOffering = _diskOfferingDao.findById(work.getNewDiskOfferingId());
3179+
3180+
Volume newVol = orchestrateMigrateVolume(volume, targetStoragePool, work.isLiveMigrate(), newDiskOffering);
3181+
3182+
return new Pair<JobInfo.Status, String>(JobInfo.Status.SUCCEEDED, _jobMgr.marshallResultObject(newVol.getId()));
31233183
}
31243184

31253185
@ReflectionUse

ui/l10n/ar.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,8 @@ var dictionary = {
664664
"label.disk.iops.write.rate": "Disk Write Rate (IOPS)",
665665
"label.disk.offering": "Disk Offering",
666666
"label.disk.offering.details": "Disk offering details",
667+
"label.disk.newOffering": "New Disk Offering",
668+
"label.disk.newOffering.description": "New disk offering to be used by this volume after the migration.",
667669
"label.disk.physicalsize":"Physical Size",
668670
"label.disk.provisioningtype": "Provisioning Type",
669671
"label.disk.read.bytes": "Disk Read (Bytes)",
@@ -1089,6 +1091,8 @@ var dictionary = {
10891091
"label.migrate.to.host": "التحول إلى المضيف",
10901092
"label.migrate.to.storage": "التحول إلى التخزين",
10911093
"label.migrate.volume": "Migrate Volume",
1094+
"label.migrate.volume.newDiskOffering": "Replace disk offering?",
1095+
"label.migrate.volume.newDiskOffering.desc": "This option allows administrators to replace the old disk offering, using one that better suits the new placement of the volume.",
10921096
"label.migrate.volume.to.primary.storage": "Migrate volume to another primary storage",
10931097
"label.min.instances": "Min Instances",
10941098
"label.min.past.the.hr": "min past the hr",

0 commit comments

Comments
 (0)