diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index b060b5a21762..3bd5ecdbe751 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -29,6 +29,7 @@ public class ApiConstants { public static final String ANNOTATION = "annotation"; public static final String API_KEY = "apikey"; public static final String ASYNC_BACKUP = "asyncbackup"; + public static final String AUTO_SELECT = "autoselect"; public static final String USER_API_KEY = "userapikey"; public static final String APPLIED = "applied"; public static final String LIST_LB_VMIPS = "lbvmips"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java index 50129a580b31..decc722e86f1 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/systemvm/MigrateSystemVMCmd.java @@ -30,6 +30,7 @@ import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.SystemVmResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang.BooleanUtils; import org.apache.log4j.Logger; import com.cloud.event.EventTypes; @@ -75,6 +76,12 @@ public class MigrateSystemVMCmd extends BaseAsyncCmd { description = "Destination storage pool ID to migrate VM volumes to. Required for migrating the root disk volume") private Long storageId; + @Parameter(name = ApiConstants.AUTO_SELECT, + since = "4.16.0", + type = CommandType.BOOLEAN, + description = "Automatically select a destination host which do not require storage migration, if hostId and storageId are not specified. false by default") + private Boolean autoSelect; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -91,6 +98,10 @@ public Long getStorageId() { return storageId; } + public Boolean isAutoSelect() { + return BooleanUtils.isNotFalse(autoSelect); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -122,27 +133,14 @@ public String getEventDescription() { @Override public void execute() { - if (getHostId() == null && getStorageId() == null) { - throw new InvalidParameterValueException("Either hostId or storageId must be specified"); - } - if (getHostId() != null && getStorageId() != null) { throw new InvalidParameterValueException("Only one of hostId and storageId can be specified"); } + try { //FIXME : Should not be calling UserVmService to migrate all types of VMs - need a generic VM layer VirtualMachine migratedVm = null; - if (getHostId() != null) { - Host destinationHost = _resourceService.getHost(getHostId()); - if (destinationHost == null) { - throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId()); - } - if (destinationHost.getType() != Host.Type.Routing) { - throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one"); - } - CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); - migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), destinationHost, new HashMap()); - } else if (getStorageId() != null) { + if (getStorageId() != null) { // OfflineMigration performed when this parameter is specified StoragePool destStoragePool = _storageService.getStoragePool(getStorageId()); if (destStoragePool == null) { @@ -150,6 +148,25 @@ public void execute() { } CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStorageId()); migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), destStoragePool); + } else { + Host destinationHost = null; + if (getHostId() != null) { + destinationHost =_resourceService.getHost(getHostId()); + if (destinationHost == null) { + throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId()); + } + if (destinationHost.getType() != Host.Type.Routing) { + throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one"); + } + } else if (! isAutoSelect()) { + 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"); + } + CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); + if (destinationHost == null) { + migratedVm = _userVmService.migrateVirtualMachine(getVirtualMachineId(), null); + } else { + migratedVm = _userVmService.migrateVirtualMachineWithVolume(getVirtualMachineId(), destinationHost, new HashMap()); + } } if (migratedVm != null) { // return the generic system VM instance response diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java index 9f73ae586a08..2c68d86f4450 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/MigrateVMCmd.java @@ -29,6 +29,7 @@ import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; +import org.apache.commons.lang.BooleanUtils; import com.cloud.event.EventTypes; import com.cloud.exception.ConcurrentOperationException; @@ -60,7 +61,7 @@ public class MigrateVMCmd extends BaseAsyncCmd { type = CommandType.UUID, entityType = HostResponse.class, required = false, - description = "Destination Host ID to migrate VM to. Required for live migrating a VM from host to host") + description = "Destination Host ID to migrate VM to.") private Long hostId; @Parameter(name = ApiConstants.VIRTUAL_MACHINE_ID, @@ -77,6 +78,12 @@ public class MigrateVMCmd extends BaseAsyncCmd { description = "Destination storage pool ID to migrate VM volumes to. Required for migrating the root disk volume") private Long storageId; + @Parameter(name = ApiConstants.AUTO_SELECT, + since = "4.16.0", + type = CommandType.BOOLEAN, + description = "Automatically select a destination host which do not require storage migration, if hostId and storageId are not specified. false by default") + private Boolean autoSelect; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -93,6 +100,10 @@ public Long getStoragePoolId() { return storageId; } + public Boolean isAutoSelect() { + return BooleanUtils.isNotFalse(autoSelect); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -132,10 +143,6 @@ public String getEventDescription() { @Override public void execute() { - if (getHostId() == null && getStoragePoolId() == null) { - throw new InvalidParameterValueException("Either hostId or storageId must be specified"); - } - if (getHostId() != null && getStoragePoolId() != null) { throw new InvalidParameterValueException("Only one of hostId and storageId can be specified"); } @@ -146,17 +153,6 @@ public void execute() { } Host destinationHost = null; - if (getHostId() != null) { - destinationHost = _resourceService.getHost(getHostId()); - if (destinationHost == null) { - throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId()); - } - if (destinationHost.getType() != Host.Type.Routing) { - throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one"); - } - CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); - } - // OfflineMigration performed when this parameter is specified StoragePool destStoragePool = null; if (getStoragePoolId() != null) { @@ -165,13 +161,24 @@ public void execute() { throw new InvalidParameterValueException("Unable to find the storage pool to migrate the VM"); } CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to storage pool Id: " + getStoragePoolId()); + } else if (getHostId() != null) { + destinationHost = _resourceService.getHost(getHostId()); + if (destinationHost == null) { + throw new InvalidParameterValueException("Unable to find the host to migrate the VM, host id=" + getHostId()); + } + if (destinationHost.getType() != Host.Type.Routing) { + throw new InvalidParameterValueException("The specified host(" + destinationHost.getName() + ") is not suitable to migrate the VM, please specify another one"); + } + CallContext.current().setEventDetails("VM Id: " + getVirtualMachineId() + " to host Id: " + getHostId()); + } else if (! isAutoSelect()) { + 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"); } try { VirtualMachine migratedVm = null; - if (getHostId() != null) { + if (getStoragePoolId() == null) { migratedVm = _userVmService.migrateVirtualMachine(getVirtualMachineId(), destinationHost); - } else if (getStoragePoolId() != null) { + } else { migratedVm = _userVmService.vmStorageMigration(getVirtualMachineId(), destStoragePool); } if (migratedVm != null) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index c56a82681d9f..56f49cb5fbe6 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -189,11 +189,13 @@ import com.cloud.event.UsageEventUtils; import com.cloud.event.UsageEventVO; import com.cloud.event.dao.UsageEventDao; +import com.cloud.exception.AffinityConflictException; import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.CloudException; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.InsufficientServerCapacityException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.ManagementServerException; import com.cloud.exception.OperationTimedoutException; @@ -977,8 +979,7 @@ private UserVm rebootVirtualMachine(long userId, long vmId, boolean enterSetup, } if (vm.getState() == State.Running && vm.getHostId() != null) { - collectVmDiskStatistics(vm); - collectVmNetworkStatistics(vm); + collectVmDiskAndNetworkStatistics(vm, State.Running); if (forced) { Host vmOnHost = _hostDao.findById(vm.getHostId()); @@ -5961,8 +5962,47 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr throw new InvalidParameterValueException("Cannot migrate VM, host with id: " + srcHostId + " for VM not found"); } + DeployDestination dest = null; + if (destinationHost == null) { + dest = chooseVmMigrationDestination(vm, srcHost); + } else { + dest = checkVmMigrationDestination(vm, srcHost, destinationHost); + } - if (destinationHost.getId() == srcHostId) { + // If no suitable destination found then throw exception + if (dest == null) { + throw new CloudRuntimeException("Unable to find suitable destination to migrate VM " + vm.getInstanceName()); + } + + collectVmDiskAndNetworkStatistics(vmId, State.Running); + _itMgr.migrate(vm.getUuid(), srcHostId, dest); + return findMigratedVm(vm.getId(), vm.getType()); + } + + private DeployDestination chooseVmMigrationDestination(VMInstanceVO vm, Host srcHost) { + vm.setLastHostId(null); // Last host does not have higher priority in vm migration + final ServiceOfferingVO offering = _offeringDao.findById(vm.getId(), vm.getServiceOfferingId()); + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(vm, null, offering, null, null); + final Long srcHostId = srcHost.getId(); + final Host host = _hostDao.findById(srcHostId); + final DataCenterDeployment plan = new DataCenterDeployment(host.getDataCenterId(), host.getPodId(), host.getClusterId(), null, null, null); + ExcludeList excludes = new ExcludeList(); + excludes.addHost(srcHostId); + try { + return _planningMgr.planDeployment(profile, plan, excludes, null); + } catch (final AffinityConflictException e2) { + s_logger.warn("Unable to create deployment, affinity rules associted to the VM conflict", e2); + throw new CloudRuntimeException("Unable to create deployment, affinity rules associted to the VM conflict"); + } catch (final InsufficientServerCapacityException e3) { + throw new CloudRuntimeException("Unable to find a server to migrate the vm to"); + } + } + + private DeployDestination checkVmMigrationDestination(VMInstanceVO vm, Host srcHost, Host destinationHost) throws VirtualMachineMigrationException { + if (destinationHost == null) { + return null; + } + if (destinationHost.getId() == srcHost.getId()) { throw new InvalidParameterValueException("Cannot migrate VM, VM is already present on this host, please specify valid destination host to migrate the VM"); } @@ -5983,7 +6023,7 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr throw new CloudRuntimeException("Cannot migrate VM, VM is DPDK enabled VM but destination host is not DPDK enabled"); } - checkHostsDedication(vm, srcHostId, destinationHost.getId()); + checkHostsDedication(vm, srcHost.getId(), destinationHost.getId()); // call to core process DataCenterVO dcVO = _dcDao.findById(destinationHost.getDataCenterId()); @@ -6002,19 +6042,14 @@ public VirtualMachine migrateVirtualMachine(Long vmId, Host destinationHost) thr + " already has max Running VMs(count includes system VMs), cannot migrate to this host"); } //check if there are any ongoing volume snapshots on the volumes associated with the VM. + Long vmId = vm.getId(); s_logger.debug("Checking if there are any ongoing snapshots volumes associated with VM with ID " + vmId); if (checkStatusOfVolumeSnapshots(vmId, null)) { 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."); } s_logger.debug("Found no ongoing snapshots on volumes associated with the vm with id " + vmId); - UserVmVO uservm = _vmDao.findById(vmId); - if (uservm != null) { - collectVmDiskStatistics(uservm); - collectVmNetworkStatistics(uservm); - } - _itMgr.migrate(vm.getUuid(), srcHostId, dest); - return findMigratedVm(vm.getId(), vm.getType()); + return dest; } private boolean isOnSupportedHypevisorForMigration(VMInstanceVO vm) { @@ -7377,11 +7412,7 @@ else if (!answer.getResult()) { @Override public void prepareStop(VirtualMachineProfile profile) { - UserVmVO vm = _vmDao.findById(profile.getId()); - if (vm != null && vm.getState() == State.Stopping) { - collectVmDiskStatistics(vm); - collectVmNetworkStatistics(vm); - } + collectVmDiskAndNetworkStatistics(profile.getId(), State.Stopping); } @Override @@ -7724,4 +7755,22 @@ private Network getNetworkForOvfNetworkMapping(DataCenter zone, Account owner) t } return network; } + + private void collectVmDiskAndNetworkStatistics(Long vmId, State expectedState) { + UserVmVO uservm = _vmDao.findById(vmId); + if (uservm != null) { + collectVmDiskAndNetworkStatistics(uservm, expectedState); + } else { + s_logger.info(String.format("Skip collecting vm %s disk and network statistics as it is not user vm", uservm)); + } + } + + private void collectVmDiskAndNetworkStatistics(UserVm vm, State expectedState) { + if (expectedState == null || expectedState == vm.getState()) { + collectVmDiskStatistics(vm); + collectVmNetworkStatistics(vm); + } else { + 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())); + } + } } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index 178f2550c56d..0456c8cb1a1a 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1392,6 +1392,7 @@ "label.metrics.network.write": "Write", "label.metrics.num.cpu.cores": "Cores", "label.migrate.allowed": "Migrate Allowed", +"label.migrate.auto.select": "AutoSelect", "label.migrate.data.from.image.store": "Migrate Data from Image store", "label.migrate.instance.to": "Migrate instance to", "label.migrate.instance.to.host": "Migrate instance to another host", diff --git a/ui/src/views/compute/MigrateWizard.vue b/ui/src/views/compute/MigrateWizard.vue index 1b826397d368..e0f390b0a3a1 100644 --- a/ui/src/views/compute/MigrateWizard.vue +++ b/ui/src/views/compute/MigrateWizard.vue @@ -46,7 +46,9 @@ v-else />
- {{ record.memoryused | byteToGigabyte }} GB + + {{ record.memoryused | byteToGigabyte }} GB +
{{ record.memoryallocatedpercentage }} @@ -169,6 +171,12 @@ export default { this.hosts.sort((a, b) => { return b.suitableformigration - a.suitableformigration }) + for (const key in this.hosts) { + if (this.hosts[key].suitableformigration && !this.hosts[key].requiresstoragemigration) { + this.hosts.unshift({ id: -1, name: this.$t('label.migrate.auto.select'), suitableformigration: true, requiresstoragemigration: false }) + break + } + } this.totalCount = response.findhostsformigrationresponse.count }).catch(error => { this.$message.error(`${this.$t('message.load.host.failed')}: ${error}`) @@ -186,10 +194,9 @@ export default { var migrateApi = isUserVm ? this.selectedHost.requiresStorageMotion ? 'migrateVirtualMachineWithVolume' : 'migrateVirtualMachine' : 'migrateSystemVm' - api(migrateApi, { - hostid: this.selectedHost.id, - virtualmachineid: this.resource.id - }).then(response => { + var migrateParams = this.selectedHost.id === -1 ? { autoselect: true, virtualmachineid: this.resource.id } + : { hostid: this.selectedHost.id, virtualmachineid: this.resource.id } + api(migrateApi, migrateParams).then(response => { const jobid = this.selectedHost.requiresStorageMotion ? response.migratevirtualmachinewithvolumeresponse.jobid : response.migratevirtualmachineresponse.jobid this.$pollJob({ jobId: jobid,