Skip to content

Commit 846efdb

Browse files
server: Optional destination host when migrate a vm (#4378)
* server: Optional destination host when migrate a vm * #4378: migrate systemvms/routers with optional host * #4378: fix mistake * #4378: fix issue when migrate systemvm * #4378 add autoselect to migrate api commands * #4378: more ui change * #4378: add label label.migrate.auto.select * #4378: add method chooseVmMigrationDestination * #4378: fix vm migration wih storageid on vmware * #4378: add method to collect vm disk/network statistics * #4378: set autoSelect to default 'true' * #4378: use BooleanUtils.isNotFalse Co-authored-by: Wei Zhou <weizhou@apache.org>
1 parent d5015d7 commit 846efdb

File tree

6 files changed

+136
-54
lines changed

6 files changed

+136
-54
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
@@ -29,6 +29,7 @@ public class ApiConstants {
2929
public static final String ANNOTATION = "annotation";
3030
public static final String API_KEY = "apikey";
3131
public static final String ASYNC_BACKUP = "asyncbackup";
32+
public static final String AUTO_SELECT = "autoselect";
3233
public static final String USER_API_KEY = "userapikey";
3334
public static final String APPLIED = "applied";
3435
public static final String LIST_LB_VMIPS = "lbvmips";

api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import org.apache.cloudstack.api.response.StoragePoolResponse;
3131
import org.apache.cloudstack.api.response.SystemVmResponse;
3232
import org.apache.cloudstack.context.CallContext;
33+
import org.apache.commons.lang.BooleanUtils;
3334
import org.apache.log4j.Logger;
3435

3536
import com.cloud.event.EventTypes;
@@ -75,6 +76,12 @@ public class MigrateSystemVMCmd extends BaseAsyncCmd {
7576
description = "Destination storage pool ID to migrate VM volumes to. Required for migrating the root disk volume")
7677
private Long storageId;
7778

79+
@Parameter(name = ApiConstants.AUTO_SELECT,
80+
since = "4.16.0",
81+
type = CommandType.BOOLEAN,
82+
description = "Automatically select a destination host which do not require storage migration, if hostId and storageId are not specified. false by default")
83+
private Boolean autoSelect;
84+
7885
/////////////////////////////////////////////////////
7986
/////////////////// Accessors ///////////////////////
8087
/////////////////////////////////////////////////////
@@ -91,6 +98,10 @@ public Long getStorageId() {
9198
return storageId;
9299
}
93100

101+
public Boolean isAutoSelect() {
102+
return BooleanUtils.isNotFalse(autoSelect);
103+
}
104+
94105
/////////////////////////////////////////////////////
95106
/////////////// API Implementation///////////////////
96107
/////////////////////////////////////////////////////
@@ -122,34 +133,40 @@ public String getEventDescription() {
122133

123134
@Override
124135
public void execute() {
125-
if (getHostId() == null && getStorageId() == null) {
126-
throw new InvalidParameterValueException("Either hostId or storageId must be specified");
127-
}
128-
129136
if (getHostId() != null && getStorageId() != null) {
130137
throw new InvalidParameterValueException("Only one of hostId and storageId can be specified");
131138
}
139+
132140
try {
133141
//FIXME : Should not be calling UserVmService to migrate all types of VMs - need a generic VM layer
134142
VirtualMachine migratedVm = null;
135-
if (getHostId() != null) {
136-
Host destinationHost = _resourceService.getHost(getHostId());
137-
if (destinationHost == null) {
138-
throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId());
139-
}
140-
if (destinationHost.getType() != Host.Type.Routing) {
141-
throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one");
142-
}
143-
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId());
144-
migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), destinationHost, new HashMap<String, String>());
145-
} else if (getStorageId() != null) {
143+
if (getStorageId() != null) {
146144
// OfflineMigration performed when this parameter is specified
147145
StoragePool destStoragePool = _storageService.getStoragePool(getStorageId());
148146
if (destStoragePool == null) {
149147
throw new InvalidParameterValueException("Unable to find the storage pool to migrate the VM");
150148
}
151149
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStorageId());
152150
migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), destStoragePool);
151+
} else {
152+
Host destinationHost = null;
153+
if (getHostId() != null) {
154+
destinationHost =_resourceService.getHost(getHostId());
155+
if (destinationHost == null) {
156+
throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId());
157+
}
158+
if (destinationHost.getType() != Host.Type.Routing) {
159+
throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one");
160+
}
161+
} else if (! isAutoSelect()) {
162+
throw new InvalidParameterValueException("Please specify a host or storage as destination, or pass 'autoselect=true' to automatically select a destination host which do not require storage migration");
163+
}
164+
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId());
165+
if (destinationHost == null) {
166+
migratedVm = _userVmService.migrateVirtualMachine(getVirtualMachineId(), null);
167+
} else {
168+
migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), destinationHost, new HashMap<String, String>());
169+
}
153170
}
154171
if (migratedVm != null) {
155172
// return the generic system VM instance response

api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.apache.cloudstack.api.response.StoragePoolResponse;
3030
import org.apache.cloudstack.api.response.UserVmResponse;
3131
import org.apache.cloudstack.context.CallContext;
32+
import org.apache.commons.lang.BooleanUtils;
3233

3334
import com.cloud.event.EventTypes;
3435
import com.cloud.exception.ConcurrentOperationException;
@@ -60,7 +61,7 @@ public class MigrateVMCmd extends BaseAsyncCmd {
6061
type = CommandType.UUID,
6162
entityType = HostResponse.class,
6263
required = false,
63-
description = "Destination Host ID to migrate VM to. Required for live migrating a VM from host to host")
64+
description = "Destination Host ID to migrate VM to.")
6465
private Long hostId;
6566

6667
@Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID,
@@ -77,6 +78,12 @@ public class MigrateVMCmd extends BaseAsyncCmd {
7778
description = "Destination storage pool ID to migrate VM volumes to. Required for migrating the root disk volume")
7879
private Long storageId;
7980

81+
@Parameter(name = ApiConstants.AUTO_SELECT,
82+
since = "4.16.0",
83+
type = CommandType.BOOLEAN,
84+
description = "Automatically select a destination host which do not require storage migration, if hostId and storageId are not specified. false by default")
85+
private Boolean autoSelect;
86+
8087
/////////////////////////////////////////////////////
8188
/////////////////// Accessors ///////////////////////
8289
/////////////////////////////////////////////////////
@@ -93,6 +100,10 @@ public Long getStoragePoolId() {
93100
return storageId;
94101
}
95102

103+
public Boolean isAutoSelect() {
104+
return BooleanUtils.isNotFalse(autoSelect);
105+
}
106+
96107
/////////////////////////////////////////////////////
97108
/////////////// API Implementation///////////////////
98109
/////////////////////////////////////////////////////
@@ -132,10 +143,6 @@ public String getEventDescription() {
132143

133144
@Override
134145
public void execute() {
135-
if (getHostId() == null && getStoragePoolId() == null) {
136-
throw new InvalidParameterValueException("Either hostId or storageId must be specified");
137-
}
138-
139146
if (getHostId() != null && getStoragePoolId() != null) {
140147
throw new InvalidParameterValueException("Only one of hostId and storageId can be specified");
141148
}
@@ -146,17 +153,6 @@ public void execute() {
146153
}
147154

148155
Host destinationHost = null;
149-
if (getHostId() != null) {
150-
destinationHost = _resourceService.getHost(getHostId());
151-
if (destinationHost == null) {
152-
throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId());
153-
}
154-
if (destinationHost.getType() != Host.Type.Routing) {
155-
throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one");
156-
}
157-
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId());
158-
}
159-
160156
// OfflineMigration performed when this parameter is specified
161157
StoragePool destStoragePool = null;
162158
if (getStoragePoolId() != null) {
@@ -165,13 +161,24 @@ public void execute() {
165161
throw new InvalidParameterValueException("Unable to find the storage pool to migrate the VM");
166162
}
167163
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStoragePoolId());
164+
} else if (getHostId() != null) {
165+
destinationHost = _resourceService.getHost(getHostId());
166+
if (destinationHost == null) {
167+
throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId());
168+
}
169+
if (destinationHost.getType() != Host.Type.Routing) {
170+
throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one");
171+
}
172+
CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId());
173+
} else if (! isAutoSelect()) {
174+
throw new InvalidParameterValueException("Please specify a host or storage as destination, or pass 'autoselect=true' to automatically select a destination host which do not require storage migration");
168175
}
169176

170177
try {
171178
VirtualMachine migratedVm = null;
172-
if (getHostId() != null) {
179+
if (getStoragePoolId() == null) {
173180
migratedVm = _userVmService.migrateVirtualMachine(getVirtualMachineId(), destinationHost);
174-
} else if (getStoragePoolId() != null) {
181+
} else {
175182
migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), destStoragePool);
176183
}
177184
if (migratedVm != null) {

server/src/main/java/com/cloud/vm/UserVmManagerImpl.java

Lines changed: 65 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,13 @@
189189
import com.cloud.event.UsageEventUtils;
190190
import com.cloud.event.UsageEventVO;
191191
import com.cloud.event.dao.UsageEventDao;
192+
import com.cloud.exception.AffinityConflictException;
192193
import com.cloud.exception.AgentUnavailableException;
193194
import com.cloud.exception.CloudException;
194195
import com.cloud.exception.ConcurrentOperationException;
195196
import com.cloud.exception.InsufficientAddressCapacityException;
196197
import com.cloud.exception.InsufficientCapacityException;
198+
import com.cloud.exception.InsufficientServerCapacityException;
197199
import com.cloud.exception.InvalidParameterValueException;
198200
import com.cloud.exception.ManagementServerException;
199201
import com.cloud.exception.OperationTimedoutException;
@@ -980,8 +982,7 @@ private UserVm rebootVirtualMachine(long userId, long vmId, boolean enterSetup,
980982
}
981983

982984
if (vm.getState() == State.Running && vm.getHostId() != null) {
983-
collectVmDiskStatistics(vm);
984-
collectVmNetworkStatistics(vm);
985+
collectVmDiskAndNetworkStatistics(vm, State.Running);
985986

986987
if (forced) {
987988
Host vmOnHost = _hostDao.findById(vm.getHostId());
@@ -5970,8 +5971,47 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr
59705971
throw new InvalidParameterValueException("Cannot migrate VM, host with id: " + srcHostId + " for VM not found");
59715972
}
59725973

5974+
DeployDestination dest = null;
5975+
if (destinationHost == null) {
5976+
dest = chooseVmMigrationDestination(vm, srcHost);
5977+
} else {
5978+
dest = checkVmMigrationDestination(vm, srcHost, destinationHost);
5979+
}
59735980

5974-
if (destinationHost.getId() == srcHostId) {
5981+
// If no suitable destination found then throw exception
5982+
if (dest == null) {
5983+
throw new CloudRuntimeException("Unable to find suitable destination to migrate VM " + vm.getInstanceName());
5984+
}
5985+
5986+
collectVmDiskAndNetworkStatistics(vmId, State.Running);
5987+
_itMgr.migrate(vm.getUuid(), srcHostId, dest);
5988+
return findMigratedVm(vm.getId(), vm.getType());
5989+
}
5990+
5991+
private DeployDestination chooseVmMigrationDestination(VMInstanceVO vm, Host srcHost) {
5992+
vm.setLastHostId(null); // Last host does not have higher priority in vm migration
5993+
final ServiceOfferingVO offering = _offeringDao.findById(vm.getId(), vm.getServiceOfferingId());
5994+
final VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm, null, offering, null, null);
5995+
final Long srcHostId = srcHost.getId();
5996+
final Host host = _hostDao.findById(srcHostId);
5997+
final DataCenterDeployment plan = new DataCenterDeployment(host.getDataCenterId(), host.getPodId(), host.getClusterId(), null, null, null);
5998+
ExcludeList excludes = new ExcludeList();
5999+
excludes.addHost(srcHostId);
6000+
try {
6001+
return _planningMgr.planDeployment(profile, plan, excludes, null);
6002+
} catch (final AffinityConflictException e2) {
6003+
s_logger.warn("Unable to create deployment, affinity rules associted to the VM conflict", e2);
6004+
throw new CloudRuntimeException("Unable to create deployment, affinity rules associted to the VM conflict");
6005+
} catch (final InsufficientServerCapacityException e3) {
6006+
throw new CloudRuntimeException("Unable to find a server to migrate the vm to");
6007+
}
6008+
}
6009+
6010+
private DeployDestination checkVmMigrationDestination(VMInstanceVO vm, Host srcHost, Host destinationHost) throws VirtualMachineMigrationException {
6011+
if (destinationHost == null) {
6012+
return null;
6013+
}
6014+
if (destinationHost.getId() == srcHost.getId()) {
59756015
throw new InvalidParameterValueException("Cannot migrate VM, VM is already present on this host, please specify valid destination host to migrate the VM");
59766016
}
59776017

@@ -5992,7 +6032,7 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr
59926032
throw new CloudRuntimeException("Cannot migrate VM, VM is DPDK enabled VM but destination host is not DPDK enabled");
59936033
}
59946034

5995-
checkHostsDedication(vm, srcHostId, destinationHost.getId());
6035+
checkHostsDedication(vm, srcHost.getId(), destinationHost.getId());
59966036

59976037
// call to core process
59986038
DataCenterVO dcVO = _dcDao.findById(destinationHost.getDataCenterId());
@@ -6011,19 +6051,14 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr
60116051
+ " already has max Running VMs(count includes system VMs), cannot migrate to this host");
60126052
}
60136053
//check if there are any ongoing volume snapshots on the volumes associated with the VM.
6054+
Long vmId = vm.getId();
60146055
s_logger.debug("Checking if there are any ongoing snapshots volumes associated with VM with ID " + vmId);
60156056
if (checkStatusOfVolumeSnapshots(vmId, null)) {
60166057
throw new CloudRuntimeException("There is/are unbacked up snapshot(s) on volume(s) attached to this VM, VM Migration is not permitted, please try again later.");
60176058
}
60186059
s_logger.debug("Found no ongoing snapshots on volumes associated with the vm with id " + vmId);
60196060

6020-
UserVmVO uservm = _vmDao.findById(vmId);
6021-
if (uservm != null) {
6022-
collectVmDiskStatistics(uservm);
6023-
collectVmNetworkStatistics(uservm);
6024-
}
6025-
_itMgr.migrate(vm.getUuid(), srcHostId, dest);
6026-
return findMigratedVm(vm.getId(), vm.getType());
6061+
return dest;
60276062
}
60286063

60296064
private boolean isOnSupportedHypevisorForMigration(VMInstanceVO vm) {
@@ -7386,11 +7421,7 @@ else if (!answer.getResult()) {
73867421

73877422
@Override
73887423
public void prepareStop(VirtualMachineProfile profile) {
7389-
UserVmVO vm = _vmDao.findById(profile.getId());
7390-
if (vm != null && vm.getState() == State.Stopping) {
7391-
collectVmDiskStatistics(vm);
7392-
collectVmNetworkStatistics(vm);
7393-
}
7424+
collectVmDiskAndNetworkStatistics(profile.getId(), State.Stopping);
73947425
}
73957426

73967427
@Override
@@ -7733,4 +7764,22 @@ private Network getNetworkForOvfNetworkMapping(DataCenter zone, Account owner) t
77337764
}
77347765
return network;
77357766
}
7767+
7768+
private void collectVmDiskAndNetworkStatistics(Long vmId, State expectedState) {
7769+
UserVmVO uservm = _vmDao.findById(vmId);
7770+
if (uservm != null) {
7771+
collectVmDiskAndNetworkStatistics(uservm, expectedState);
7772+
} else {
7773+
s_logger.info(String.format("Skip collecting vm %s disk and network statistics as it is not user vm", uservm));
7774+
}
7775+
}
7776+
7777+
private void collectVmDiskAndNetworkStatistics(UserVm vm, State expectedState) {
7778+
if (expectedState == null || expectedState == vm.getState()) {
7779+
collectVmDiskStatistics(vm);
7780+
collectVmNetworkStatistics(vm);
7781+
} else {
7782+
s_logger.warn(String.format("Skip collecting vm %s disk and network statistics as the expected vm state is %s but actual state is %s", vm, expectedState, vm.getState()));
7783+
}
7784+
}
77367785
}

ui/public/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,7 @@
13931393
"label.metrics.network.write": "Write",
13941394
"label.metrics.num.cpu.cores": "Cores",
13951395
"label.migrate.allowed": "Migrate Allowed",
1396+
"label.migrate.auto.select": "AutoSelect",
13961397
"label.migrate.data.from.image.store": "Migrate Data from Image store",
13971398
"label.migrate.instance.to": "Migrate instance to",
13981399
"label.migrate.instance.to.host": "Migrate instance to another host",

ui/src/views/compute/MigrateWizard.vue

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@
4646
v-else />
4747
</div>
4848
<div slot="memused" slot-scope="record">
49-
{{ record.memoryused | byteToGigabyte }} GB
49+
<span v-if="record.memoryused | byteToGigabyte">
50+
{{ record.memoryused | byteToGigabyte }} GB
51+
</span>
5052
</div>
5153
<div slot="memoryallocatedpercentage" slot-scope="record">
5254
{{ record.memoryallocatedpercentage }}
@@ -169,6 +171,12 @@ export default {
169171
this.hosts.sort((a, b) => {
170172
return b.suitableformigration - a.suitableformigration
171173
})
174+
for (const key in this.hosts) {
175+
if (this.hosts[key].suitableformigration && !this.hosts[key].requiresstoragemigration) {
176+
this.hosts.unshift({ id: -1, name: this.$t('label.migrate.auto.select'), suitableformigration: true, requiresstoragemigration: false })
177+
break
178+
}
179+
}
172180
this.totalCount = response.findhostsformigrationresponse.count
173181
}).catch(error => {
174182
this.$message.error(`${this.$t('message.load.host.failed')}: ${error}`)
@@ -186,10 +194,9 @@ export default {
186194
var migrateApi = isUserVm
187195
? this.selectedHost.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine'
188196
: 'migrateSystemVm'
189-
api(migrateApi, {
190-
hostid: this.selectedHost.id,
191-
virtualmachineid: this.resource.id
192-
}).then(response => {
197+
var migrateParams = this.selectedHost.id === -1 ? { autoselect: true, virtualmachineid: this.resource.id }
198+
: { hostid: this.selectedHost.id, virtualmachineid: this.resource.id }
199+
api(migrateApi, migrateParams).then(response => {
193200
const jobid = this.selectedHost.requiresStorageMotion ? response.migratevirtualmachinewithvolumeresponse.jobid : response.migratevirtualmachineresponse.jobid
194201
this.$pollJob({
195202
jobId: jobid,

0 commit comments

Comments
 (0)