diff --git a/cosmic-agent/src/main/java/com/cloud/agent/resource/kvm/wrapper/LibvirtGetDomJobInfoWrapper.java b/cosmic-agent/src/main/java/com/cloud/agent/resource/kvm/wrapper/LibvirtGetDomJobInfoWrapper.java new file mode 100644 index 0000000000..87f3394194 --- /dev/null +++ b/cosmic-agent/src/main/java/com/cloud/agent/resource/kvm/wrapper/LibvirtGetDomJobInfoWrapper.java @@ -0,0 +1,46 @@ +package com.cloud.agent.resource.kvm.wrapper; + +import com.cloud.agent.resource.kvm.LibvirtComputingResource; +import com.cloud.common.request.ResourceWrapper; +import com.cloud.legacymodel.communication.answer.Answer; +import com.cloud.legacymodel.communication.answer.MigrationProgressAnswer; +import com.cloud.legacymodel.communication.command.MigrationProgressCommand; + +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainJobInfo; +import org.libvirt.LibvirtException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ResourceWrapper(handles = MigrationProgressCommand.class) +public final class LibvirtGetDomJobInfoWrapper extends LibvirtCommandWrapper { + + private static final Logger s_logger = LoggerFactory.getLogger(LibvirtGetDomJobInfoWrapper.class); + + @Override + public Answer execute(final MigrationProgressCommand command, final LibvirtComputingResource libvirtComputingResource) { + final String vmName = command.getVmName(); + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + final Connect conn; + DomainJobInfo domainJobInfo; + Domain vm; + + try { + conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + vm = libvirtComputingResource.getDomain(conn, vmName); + domainJobInfo = vm.getJobInfo(); + } catch (LibvirtException e) { + final String msg = " Getting domain job info failed due to " + e.toString(); + s_logger.warn(msg, e); + return new MigrationProgressAnswer(command, false, msg); + } + + return new MigrationProgressAnswer(command, true, null, + domainJobInfo.getTimeElapsed(), domainJobInfo.getTimeRemaining(), + domainJobInfo.getDataTotal(), domainJobInfo.getDataProcessed(), domainJobInfo.getDataRemaining(), + domainJobInfo.getMemTotal(), domainJobInfo.getMemProcessed(), domainJobInfo.getMemRemaining(), + domainJobInfo.getFileTotal(), domainJobInfo.getFileProcessed(), domainJobInfo.getFileRemaining() + ); + } +} diff --git a/cosmic-client/src/main/resources/commands.properties b/cosmic-client/src/main/resources/commands.properties index 2777f89f16..a3050042b5 100644 --- a/cosmic-client/src/main/resources/commands.properties +++ b/cosmic-client/src/main/resources/commands.properties @@ -54,6 +54,7 @@ migrateVirtualMachineWithVolume=1 recoverVirtualMachine=15 expungeVirtualMachine=15 getVirtualMachineUserData=15 +getVmProgress=15 #### snapshot commands createSnapshot=15 listSnapshots=31 diff --git a/cosmic-client/src/main/webapp/css/cloudstack3.css b/cosmic-client/src/main/webapp/css/cloudstack3.css index cd1c6420e7..e7edaedc97 100644 --- a/cosmic-client/src/main/webapp/css/cloudstack3.css +++ b/cosmic-client/src/main/webapp/css/cloudstack3.css @@ -12429,3 +12429,53 @@ div.gpugroups div.list-view { background: transparent url("../images/icons.png") no-repeat -626px -209px; padding: 0 0 3px 18px; } + +.ui-progressbar { + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + border: none; + border-radius: 5px; + width: 25%; + height: 15px; + z-index: 0; + position: relative; + background-color: #eee; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.25) inset; + display: inline-table; +} + +.ui-progressbar-value { + position: absolute; + width: 0; + height: inherit; + background-image: -webkit-linear-gradient(-45deg, transparent 33%, rgba(0, 0, 0, .1) 33%, rgba(0, 0, 0, .1) 66%, transparent 66%), -webkit-linear-gradient(top, rgba(255, 255, 255, .66), rgba(37, 79, 163, .66)); + border-radius: inherit; + background-size: 35px 20px, 100% 100%, 100% 100%; + text-align: center; + -webkit-animation: animate-stripes 5s linear infinite; + animation: animate-stripes 5s linear infinite; + line-height: 15px; +} + +.ui-progressbar-value span { + font-size: larger; +} + +.progress-grid { + display: flex !important; + align-content: center; + grid-gap: 10px; +} + +@-webkit-keyframes animate-stripes { + 100% { + background-position: 100px 0px; + } +} + +@keyframes animate-stripes { + 100% { + background-position: 100px 0px; + } +}⏎ diff --git a/cosmic-client/src/main/webapp/scripts/instances.js b/cosmic-client/src/main/webapp/scripts/instances.js index f3b88dfa4f..44e5b906f0 100644 --- a/cosmic-client/src/main/webapp/scripts/instances.js +++ b/cosmic-client/src/main/webapp/scripts/instances.js @@ -2098,19 +2098,21 @@ }, state: { label: 'label.state', + pollMigrationProgress: true, pollAgainIfValueIsIn: { 'Starting': 1, - 'Stopping': 1 + 'Stopping': 1, + 'Migrating': 1 }, pollAgainFn: function (context) { var toClearInterval = false; $.ajax({ - url: createURL("listVirtualMachines&id=" + context.instances[0].id), + url: createURL("listVirtualMachines&id=" + context.id), dataType: "json", async: false, success: function (json) { var jsonObj = json.listvirtualmachinesresponse.virtualmachine[0]; - if (jsonObj.state != context.instances[0].state) { + if (jsonObj.state !== context.state) { toClearInterval = true; //to clear interval } } diff --git a/cosmic-client/src/main/webapp/scripts/ui/widgets/detailView.js b/cosmic-client/src/main/webapp/scripts/ui/widgets/detailView.js index ffe75eb688..2749dd8ba0 100644 --- a/cosmic-client/src/main/webapp/scripts/ui/widgets/detailView.js +++ b/cosmic-client/src/main/webapp/scripts/ui/widgets/detailView.js @@ -1132,6 +1132,40 @@ $value.data('detail-view-is-password', value.isPassword); } + if (typeof(value.pollAgainFn) === "function") { + for(let state in value.pollAgainIfValueIsIn) { + let interval_id = 0; + if (content === state) { + if (!value.pollAgainFn(data)) { + if (value.pollMigrationProgress) { + interval_id = setInterval(function () { + $.ajax({ + url: createURL("getVmProgress&uuid=" + context.instances[0].id), + dataType: "json", + async: false + }).done(function (jsonObj) { + let json = jsonObj.getvmprogressresponse.getvmprogressresponse; + if (json.timeremaining === 0) { + clearInterval(interval_id); + } else { + let percentage = (json.dataprocessed / (json.datatotal & 1) * 100); + if (percentage >= 100) { + percentage = 100; + } + $("#migration-progress").progressbar({value: percentage}) + .children('.ui-progressbar-value') + .html('' + percentage + '%'); + } + }).fail(function (json) { + clearInterval(interval_id); + }); + }, 1000); + } + } + } + } + } + return true; }); }); diff --git a/cosmic-core/api/src/main/java/com/cloud/api/command/user/vm/GetVMProgressCmd.java b/cosmic-core/api/src/main/java/com/cloud/api/command/user/vm/GetVMProgressCmd.java new file mode 100644 index 0000000000..f911bf2338 --- /dev/null +++ b/cosmic-core/api/src/main/java/com/cloud/api/command/user/vm/GetVMProgressCmd.java @@ -0,0 +1,66 @@ +package com.cloud.api.command.user.vm; + +import com.cloud.api.APICommand; +import com.cloud.api.APICommandGroup; +import com.cloud.api.ApiConstants; +import com.cloud.api.ApiErrorCode; +import com.cloud.api.BaseCmd; +import com.cloud.api.Parameter; +import com.cloud.api.ServerApiException; +import com.cloud.api.response.VmProgressResponse; +import com.cloud.legacymodel.exceptions.CloudRuntimeException; +import com.cloud.legacymodel.exceptions.InvalidParameterValueException; +import com.cloud.legacymodel.exceptions.ResourceUnavailableException; +import com.cloud.legacymodel.user.Account; +import com.cloud.uservm.UserVm; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@APICommand(name = "getVmProgress", group = APICommandGroup.VirtualMachineService, description = "Get migration progress of VM", responseObject = VmProgressResponse.class) +public class GetVMProgressCmd extends BaseCmd { + public static final Logger s_logger = LoggerFactory.getLogger(GetVMProgressCmd.class.getName()); + + private static final String COMMAND_NAME = "getvmprogressresponse"; + @Parameter(name = ApiConstants.UUID, type = BaseCmd.CommandType.STRING, required = true, description = "The UUID of the VM.") + private String uuid; + + @Override + public void execute() { + + try { + final VmProgressResponse response = _userVmService.getVmProgress(this); + response.setResponseName(getCommandName()); + response.setObjectName(getCommandName()); + setResponseObject(response); + } catch (InvalidParameterValueException e) { + s_logger.error("Invalid parameter: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Invalid parameter"); + } catch (CloudRuntimeException e) { + s_logger.error("CloudRuntimeException: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Failed to get progress"); + } catch (Exception e) { + s_logger.error("Unexpected exception: " + e.getMessage()); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Unexpected exception"); + } + } + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Override + public long getEntityOwnerId() { + final UserVm userVm = _entityMgr.findByUuid(UserVm.class, getUuid()); + if (userVm != null) { + return userVm.getAccountId(); + } + + return Account.ACCOUNT_ID_SYSTEM; // no account info given, parent this command to SYSTEM so ERROR events are tracked + } + + public String getUuid() { + return uuid; + } +} diff --git a/cosmic-core/api/src/main/java/com/cloud/api/response/VmProgressResponse.java b/cosmic-core/api/src/main/java/com/cloud/api/response/VmProgressResponse.java new file mode 100644 index 0000000000..994af9f390 --- /dev/null +++ b/cosmic-core/api/src/main/java/com/cloud/api/response/VmProgressResponse.java @@ -0,0 +1,142 @@ +package com.cloud.api.response; + +import com.cloud.api.ApiConstants; +import com.cloud.api.BaseResponse; +import com.cloud.serializer.Param; + +import com.google.gson.annotations.SerializedName; + +public class VmProgressResponse extends BaseResponse { + + @SerializedName("timeelapsed") + @Param(description = "Time elapsed") + long timeElapsed; + + @SerializedName("timeremaining") + @Param(description = "Time remaining") + long timeRemaining; + + @SerializedName("datatotal") + @Param(description = "Data total") + long dataTotal; + + @SerializedName("dataprocessed") + @Param(description = "Data processed") + long dataProcessed; + + @SerializedName("dataremaining") + @Param(description = "Data remaining") + long dataRemaining; + + @SerializedName("memorytotal") + @Param(description = "Memory total") + long memTotal; + + @SerializedName("memoryprocessed") + @Param(description = "Memory processed") + long memProcessed; + + @SerializedName("memoryremaining") + @Param(description = "Memory remaining") + long memRemaining; + + @SerializedName("filetotal") + @Param(description = "File total") + long fileTotal; + + @SerializedName("fileprocessed") + @Param(description = "File processed") + long fileProcessed; + + @SerializedName("fileremaining") + @Param(description = "File remaining") + long fileRemaining; + + public long getTimeElapsed() { + return timeElapsed; + } + + public void setTimeElapsed(final long timeElapsed) { + this.timeElapsed = timeElapsed; + } + + public long getTimeRemaining() { + return timeRemaining; + } + + public void setTimeRemaining(final long timeRemaining) { + this.timeRemaining = timeRemaining; + } + + public long getDataTotal() { + return dataTotal; + } + + public void setDataTotal(final long dataTotal) { + this.dataTotal = dataTotal; + } + + public long getDataProcessed() { + return dataProcessed; + } + + public void setDataProcessed(final long dataProcessed) { + this.dataProcessed = dataProcessed; + } + + public long getDataRemaining() { + return dataRemaining; + } + + public void setDataRemaining(final long dataRemaining) { + this.dataRemaining = dataRemaining; + } + + public long getMemTotal() { + return memTotal; + } + + public void setMemTotal(final long memTotal) { + this.memTotal = memTotal; + } + + public long getMemProcessed() { + return memProcessed; + } + + public void setMemProcessed(final long memProcessed) { + this.memProcessed = memProcessed; + } + + public long getMemRemaining() { + return memRemaining; + } + + public void setMemRemaining(final long memRemaining) { + this.memRemaining = memRemaining; + } + + public long getFileTotal() { + return fileTotal; + } + + public void setFileTotal(final long fileTotal) { + this.fileTotal = fileTotal; + } + + public long getFileProcessed() { + return fileProcessed; + } + + public void setFileProcessed(final long fileProcessed) { + this.fileProcessed = fileProcessed; + } + + public long getFileRemaining() { + return fileRemaining; + } + + public void setFileRemaining(final long fileRemaining) { + this.fileRemaining = fileRemaining; + } +} diff --git a/cosmic-core/api/src/main/java/com/cloud/vm/UserVmService.java b/cosmic-core/api/src/main/java/com/cloud/vm/UserVmService.java index d9c1595b18..a6b72fa5dd 100644 --- a/cosmic-core/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/cosmic-core/api/src/main/java/com/cloud/vm/UserVmService.java @@ -6,6 +6,7 @@ import com.cloud.api.command.user.vm.AddNicToVMCmd; import com.cloud.api.command.user.vm.DeployVMCmd; import com.cloud.api.command.user.vm.DestroyVMCmd; +import com.cloud.api.command.user.vm.GetVMProgressCmd; import com.cloud.api.command.user.vm.RebootVMCmd; import com.cloud.api.command.user.vm.RemoveNicFromVMCmd; import com.cloud.api.command.user.vm.ResetVMPasswordCmd; @@ -19,11 +20,15 @@ import com.cloud.api.command.user.vm.UpgradeVMCmd; import com.cloud.api.command.user.vmgroup.CreateVMGroupCmd; import com.cloud.api.command.user.vmgroup.DeleteVMGroupCmd; +import com.cloud.api.response.VmProgressResponse; import com.cloud.db.model.Zone; +import com.cloud.legacymodel.communication.command.MigrationProgressCommand; import com.cloud.legacymodel.dc.Host; +import com.cloud.legacymodel.exceptions.CloudRuntimeException; import com.cloud.legacymodel.exceptions.ConcurrentOperationException; import com.cloud.legacymodel.exceptions.ExecutionException; import com.cloud.legacymodel.exceptions.InsufficientCapacityException; +import com.cloud.legacymodel.exceptions.InvalidParameterValueException; import com.cloud.legacymodel.exceptions.ManagementServerException; import com.cloud.legacymodel.exceptions.ResourceAllocationException; import com.cloud.legacymodel.exceptions.ResourceUnavailableException; @@ -235,6 +240,7 @@ VirtualMachine migrateVirtualMachineWithVolume(Long vmId, Host destinationHost, UserVm expungeVm(long vmId) throws ResourceUnavailableException, ConcurrentOperationException; + VmProgressResponse getVmProgress(GetVMProgressCmd cmd) throws CloudRuntimeException, InvalidParameterValueException; /** * Finds and returns an encrypted password for a VM. * diff --git a/cosmic-core/apidoc/XmlToHtmlConverterData.java b/cosmic-core/apidoc/XmlToHtmlConverterData.java new file mode 100644 index 0000000000..63225c7182 --- /dev/null +++ b/cosmic-core/apidoc/XmlToHtmlConverterData.java @@ -0,0 +1,29 @@ +/* Generated using gen_toc.py. Do not edit. */ + +import java.util.HashSet; +import java.util.Set; + +public class XmlToHtmlConverterData { + + Set rootAdminCommandNames = new HashSet(); + Set domainAdminCommandNames = new HashSet(); + Set userCommandNames = new HashSet(); + + + public void populateForUser() { + + } + + + public void populateForRootAdmin() { + + } + + + public void populateForDomainAdmin() { + + } + + +} + diff --git a/cosmic-core/apidoc/gen_toc.py b/cosmic-core/apidoc/gen_toc.py index adf2ebebe8..ac9e84adeb 100755 --- a/cosmic-core/apidoc/gen_toc.py +++ b/cosmic-core/apidoc/gen_toc.py @@ -1,4 +1,4 @@ -#!/cygdrive/c/Python27 +#!/usr/bin/env python2.7 import os.path @@ -136,7 +136,8 @@ 'CacheStore': 'Cache Store', 'listHAWorkers': 'CloudOps', 'listWhoHasThisIp': 'CloudOps', - 'listWhoHasThisMac': 'CloudOps' + 'listWhoHasThisMac': 'CloudOps', + 'getVmProgress': 'Virtual Machine' } categories = { } diff --git a/cosmic-core/apidoc/generatetocforadmin_include.xsl b/cosmic-core/apidoc/generatetocforadmin_include.xsl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cosmic-core/apidoc/generatetocfordomainadmin_include.xsl b/cosmic-core/apidoc/generatetocfordomainadmin_include.xsl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cosmic-core/apidoc/generatetocforuser_include.xsl b/cosmic-core/apidoc/generatetocforuser_include.xsl new file mode 100644 index 0000000000..e69de29bb2 diff --git a/cosmic-core/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java b/cosmic-core/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java index 9885904760..9c85083d76 100644 --- a/cosmic-core/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java +++ b/cosmic-core/engine/api/src/main/java/com/cloud/vm/VirtualMachineManager.java @@ -4,7 +4,9 @@ import com.cloud.deploy.DeploymentPlan; import com.cloud.deploy.DeploymentPlanner; import com.cloud.framework.config.ConfigKey; +import com.cloud.legacymodel.communication.answer.MigrationProgressAnswer; import com.cloud.legacymodel.exceptions.AgentUnavailableException; +import com.cloud.legacymodel.exceptions.CloudRuntimeException; import com.cloud.legacymodel.exceptions.ConcurrentOperationException; import com.cloud.legacymodel.exceptions.InsufficientCapacityException; import com.cloud.legacymodel.exceptions.InsufficientServerCapacityException; @@ -190,6 +192,8 @@ void findHostAndMigrate(String vmUuid, Long newSvcOfferingId, DeploymentPlanner. boolean getExecuteInSequence(HypervisorType hypervisorType); + MigrationProgressAnswer getMigrationProgress(String vmUuid) throws CloudRuntimeException; + public interface Topics { public static final String VM_POWER_STATE = "vm.powerstate"; } diff --git a/cosmic-core/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java b/cosmic-core/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java index 0e1a53505c..da676506e5 100644 --- a/cosmic-core/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java +++ b/cosmic-core/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java @@ -58,6 +58,7 @@ import com.cloud.legacymodel.communication.answer.Answer; import com.cloud.legacymodel.communication.answer.CheckVirtualMachineAnswer; import com.cloud.legacymodel.communication.answer.ClusterVMMetaDataSyncAnswer; +import com.cloud.legacymodel.communication.answer.MigrationProgressAnswer; import com.cloud.legacymodel.communication.answer.PlugNicAnswer; import com.cloud.legacymodel.communication.answer.RebootAnswer; import com.cloud.legacymodel.communication.answer.RestoreVMSnapshotAnswer; @@ -69,6 +70,7 @@ import com.cloud.legacymodel.communication.command.ClusterVMMetaDataSyncCommand; import com.cloud.legacymodel.communication.command.Command; import com.cloud.legacymodel.communication.command.MigrateCommand; +import com.cloud.legacymodel.communication.command.MigrationProgressCommand; import com.cloud.legacymodel.communication.command.PingRoutingCommand; import com.cloud.legacymodel.communication.command.PlugNicCommand; import com.cloud.legacymodel.communication.command.PrepareForMigrationCommand; @@ -195,6 +197,7 @@ import java.util.Map; import java.util.TimeZone; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -235,6 +238,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac "Interval to send application level pings to make sure the connection is still working", false); static final ConfigKey DefaultDiskControllerName = new ConfigKey<>("Advanced", String.class, "vm.default.disk.controller", "SCSI", "Default disk controller type for routers, systemVMs and ", false); + static final ConfigKey VmOpProgressInterval = new ConfigKey<>("Advanced", Long.class, "vm.op.progress.interval", "1000", + "Time (in milliseconds) to wait before checking migration progress", false); private static final Logger s_logger = LoggerFactory.getLogger(VirtualMachineManagerImpl.class); private static final String VM_SYNC_ALERT_SUBJECT = "VM state sync alert"; @Inject @@ -344,6 +349,8 @@ public class VirtualMachineManagerImpl extends ManagerBase implements VirtualMac VmWorkJobHandlerProxy _jobHandlerProxy = new VmWorkJobHandlerProxy(this); Map _vmGurus = new HashMap<>(); + ConcurrentHashMap> migrationProgressQueue = new ConcurrentHashMap<>(); + ConcurrentHashMap migrationProgressAnswerMap = new ConcurrentHashMap<>(); ScheduledExecutorService _executor = null; protected VirtualMachineManagerImpl() { @@ -390,6 +397,7 @@ public boolean start() { // TODO, initial delay is hardcoded _executor.scheduleAtFixedRate(new CleanupTask(), 5, VmJobStateReportInterval.value(), TimeUnit.SECONDS); _executor.scheduleAtFixedRate(new TransitionTask(), VmOpCleanupInterval.value(), VmOpCleanupInterval.value(), TimeUnit.SECONDS); + _executor.scheduleAtFixedRate(new MonitorMigrationTask(), VmOpProgressInterval.value(), VmOpProgressInterval.value(), TimeUnit.MILLISECONDS); cancelWorkItems(_nodeId); volumeMgr.cleanupStorageJobs(); @@ -719,11 +727,13 @@ private void orchestrateMigrateWithStorage(final String vmUuid, final long srcHo _agentMgr.send(srcHost.getId(), dettachCommand); s_logger.debug("Deleted config drive ISO for vm " + vm.getInstanceName() + " In host " + srcHost); } catch (final OperationTimedoutException e) { - s_logger.debug("TIme out occured while exeuting command AttachOrDettachConfigDrive " + e.getMessage()); + s_logger.debug("Time out occurred while executing command AttachOrDettachConfigDrive " + e.getMessage()); } } } + migrationProgressQueue.put(vm.getUuid(), new Pair<>(srcHost.getId(), vm)); + // Migrate the vm and its volume. volumeMgr.migrateVolumes(vm, to, srcHost, destHost, volumeToPoolMap); @@ -747,6 +757,8 @@ private void orchestrateMigrateWithStorage(final String vmUuid, final long srcHo migrated = true; } finally { + migrationProgressQueue.remove(vm.getUuid()); + if (!migrated) { s_logger.info("Migration was unsuccessful. Cleaning up: " + vm); _alertMgr.sendAlert(alertType, srcHost.getDataCenterId(), srcHost.getPodId(), @@ -2667,6 +2679,13 @@ public boolean getExecuteInSequence(final HypervisorType hypervisorType) { } } + public MigrationProgressAnswer getMigrationProgress(String vmUuid) throws CloudRuntimeException { + if (migrationProgressAnswerMap.containsKey(vmUuid)) { + return migrationProgressAnswerMap.get(vmUuid); + } + throw new CloudRuntimeException("Unable get job for vm " + vmUuid); + } + private void orchestrateReboot(final String vmUuid, final Map params) throws InsufficientCapacityException, ConcurrentOperationException, ResourceUnavailableException { final VMInstanceVO vm = _vmDao.findByUuid(vmUuid); @@ -4672,4 +4691,33 @@ public boolean jobIsForSameNetwork(final VmWorkJobVO workJob, final Network netw } } } + + protected class MonitorMigrationTask extends ManagedContextRunnable { + public MonitorMigrationTask() { + s_logger.info("Starting Monitor Migration task"); + } + + @Override + protected void runInContext() { + try { + if (!migrationProgressQueue.isEmpty()) { + for (Map.Entry> entry: migrationProgressQueue.entrySet()) { + VMInstanceVO vmInstanceVO = entry.getValue().second(); + final MigrationProgressCommand migrationProgressCommand = new MigrationProgressCommand(vmInstanceVO.getInstanceName()); + if (vmInstanceVO.getState() == State.Migrating) { + Answer answer = _agentMgr.send(entry.getValue().first(), migrationProgressCommand); + migrationProgressAnswerMap.put(entry.getKey(), (MigrationProgressAnswer) answer); + } + } + } + TimeUnit.MILLISECONDS.sleep(VmOpProgressInterval.value()); + } catch (AgentUnavailableException e) { + s_logger.error("Agent unavailable: " + e.getMessage()); + } catch (OperationTimedoutException e) { + s_logger.error("Operation timeout: " + e.getMessage()); + } catch (Exception e) { + s_logger.error("Unexpected exception: " + e.getMessage()); + } + } + } } diff --git a/cosmic-core/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/cosmic-core/server/src/main/java/com/cloud/server/ManagementServerImpl.java index 1242d44b61..1ef3cf3c9f 100644 --- a/cosmic-core/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/cosmic-core/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -337,6 +337,7 @@ import com.cloud.api.command.user.vm.DeployVMCmd; import com.cloud.api.command.user.vm.DestroyVMCmd; import com.cloud.api.command.user.vm.GetVMPasswordCmd; +import com.cloud.api.command.user.vm.GetVMProgressCmd; import com.cloud.api.command.user.vm.ListNicsCmd; import com.cloud.api.command.user.vm.ListVMsCmd; import com.cloud.api.command.user.vm.RebootVMCmd; @@ -3699,6 +3700,7 @@ public List> getCommands() { cmdList.add(ListHAWorkersCmd.class); cmdList.add(ListWhoHasThisIpCmd.class); cmdList.add(ListWhoHasThisMacCmd.class); + cmdList.add(GetVMProgressCmd.class); // separated admin commands cmdList.add(ListAccountsCmdByAdmin.class); cmdList.add(ListZonesCmdByAdmin.class); diff --git a/cosmic-core/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/cosmic-core/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 1acab8f8ed..6cfe52e03e 100644 --- a/cosmic-core/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/cosmic-core/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -16,6 +16,7 @@ import com.cloud.api.command.user.vm.AddNicToVMCmd; import com.cloud.api.command.user.vm.DeployVMCmd; import com.cloud.api.command.user.vm.DestroyVMCmd; +import com.cloud.api.command.user.vm.GetVMProgressCmd; import com.cloud.api.command.user.vm.RebootVMCmd; import com.cloud.api.command.user.vm.RemoveNicFromVMCmd; import com.cloud.api.command.user.vm.ResetVMPasswordCmd; @@ -29,6 +30,7 @@ import com.cloud.api.command.user.vm.UpgradeVMCmd; import com.cloud.api.command.user.vmgroup.CreateVMGroupCmd; import com.cloud.api.command.user.vmgroup.DeleteVMGroupCmd; +import com.cloud.api.response.VmProgressResponse; import com.cloud.capacity.Capacity; import com.cloud.capacity.CapacityManager; import com.cloud.common.managed.context.ManagedContextRunnable; @@ -79,6 +81,7 @@ import com.cloud.legacymodel.communication.answer.Answer; import com.cloud.legacymodel.communication.answer.GetVmDiskStatsAnswer; import com.cloud.legacymodel.communication.answer.GetVmStatsAnswer; +import com.cloud.legacymodel.communication.answer.MigrationProgressAnswer; import com.cloud.legacymodel.communication.answer.RestoreVMSnapshotAnswer; import com.cloud.legacymodel.communication.answer.StartAnswer; import com.cloud.legacymodel.communication.command.Command; @@ -86,6 +89,7 @@ import com.cloud.legacymodel.communication.command.GetVmDiskStatsCommand; import com.cloud.legacymodel.communication.command.GetVmIpAddressCommand; import com.cloud.legacymodel.communication.command.GetVmStatsCommand; +import com.cloud.legacymodel.communication.command.MigrationProgressCommand; import com.cloud.legacymodel.communication.command.PvlanSetupCommand; import com.cloud.legacymodel.communication.command.RestoreVMSnapshotCommand; import com.cloud.legacymodel.configuration.Resource.ResourceType; @@ -3559,6 +3563,38 @@ public UserVm expungeVm(final long vmId) throws ResourceUnavailableException, Co } } + @Override + public VmProgressResponse getVmProgress(final GetVMProgressCmd cmd) throws CloudRuntimeException, InvalidParameterValueException { + String uuid = cmd.getUuid(); + final UserVmVO vm = _vmDao.findByUuid(uuid); + if (vm == null) { + throw new InvalidParameterValueException("Unable to find virtual machine with uuid: " + uuid); + } + //check permissions + _accountMgr.checkAccess(CallContext.current().getCallingAccount(), null, true, vm); + + try { + MigrationProgressAnswer migrationProgressAnswer = _itMgr.getMigrationProgress(uuid); + + VmProgressResponse vmProgressResponse = new VmProgressResponse(); + vmProgressResponse.setTimeElapsed(migrationProgressAnswer.getTimeElapsed()); + vmProgressResponse.setTimeRemaining(migrationProgressAnswer.getTimeRemaining()); + vmProgressResponse.setDataTotal(migrationProgressAnswer.getDataTotal()); + vmProgressResponse.setDataProcessed(migrationProgressAnswer.getDataProcessed()); + vmProgressResponse.setDataRemaining(migrationProgressAnswer.getDataRemaining()); + vmProgressResponse.setMemTotal(migrationProgressAnswer.getMemTotal()); + vmProgressResponse.setMemProcessed(migrationProgressAnswer.getMemProcessed()); + vmProgressResponse.setMemRemaining(migrationProgressAnswer.getMemRemaining()); + vmProgressResponse.setFileTotal(migrationProgressAnswer.getFileTotal()); + vmProgressResponse.setFileProcessed(migrationProgressAnswer.getFileProcessed()); + vmProgressResponse.setFileRemaining(migrationProgressAnswer.getFileRemaining()); + return vmProgressResponse; + } catch (CloudRuntimeException e) { + // Virtual Machine exists but no job is running, return empty response + return new VmProgressResponse(); + } + } + @Override public String getVmUserData(final long vmId) { final UserVmVO vm = _vmDao.findById(vmId); diff --git a/cosmic-model/src/main/java/com/cloud/legacymodel/communication/answer/MigrationProgressAnswer.java b/cosmic-model/src/main/java/com/cloud/legacymodel/communication/answer/MigrationProgressAnswer.java new file mode 100644 index 0000000000..01039960a3 --- /dev/null +++ b/cosmic-model/src/main/java/com/cloud/legacymodel/communication/answer/MigrationProgressAnswer.java @@ -0,0 +1,101 @@ +package com.cloud.legacymodel.communication.answer; + +import com.cloud.legacymodel.communication.command.MigrationProgressCommand; + +public class MigrationProgressAnswer extends Answer { + long timeElapsed; + long timeRemaining; + long dataTotal; + long dataProcessed; + long dataRemaining; + long memTotal; + long memProcessed; + long memRemaining; + long fileTotal; + long fileProcessed; + long fileRemaining; + + public MigrationProgressAnswer(final MigrationProgressCommand cmd, final boolean success, final String details) { + super(cmd, success, details); + } + + public MigrationProgressAnswer(final MigrationProgressCommand cmd, final boolean success, final String details, + final long timeElapsed, final long timeRemaining, + final long dataTotal, final long dataProcessed, final long dataRemaining, + final long memTotal, final long memProcessed, final long memRemaining, + final long fileTotal, final long fileProcessed, final long fileRemaining) { + super(cmd, success, details); + + this.timeElapsed = timeElapsed; + this.timeRemaining = timeRemaining; + this.dataTotal = dataTotal; + this.dataProcessed = dataProcessed; + this.dataRemaining = dataRemaining; + this.memTotal = memTotal; + this.memProcessed = memProcessed; + this.memRemaining = memRemaining; + this.fileTotal = fileTotal; + this.fileProcessed = fileProcessed; + this.fileRemaining = fileRemaining; + } + + public long getTimeElapsed() { + return timeElapsed; + } + + public long getTimeRemaining() { + return timeRemaining; + } + + public long getDataTotal() { + return dataTotal; + } + + public long getDataProcessed() { + return dataProcessed; + } + + public long getDataRemaining() { + return dataRemaining; + } + + public long getMemTotal() { + return memTotal; + } + + public long getMemProcessed() { + return memProcessed; + } + + public long getMemRemaining() { + return memRemaining; + } + + public long getFileTotal() { + return fileTotal; + } + + public long getFileProcessed() { + return fileProcessed; + } + + public long getFileRemaining() { + return fileRemaining; + } + + public String String() { + return "MigrationProgressAnswer{" + + "timeElapsed=" + timeElapsed + + ", timeRemaining=" + timeRemaining + + ", dataTotal=" + dataTotal + + ", dataProcessed=" + dataProcessed + + ", dataRemaining=" + dataRemaining + + ", memTotal=" + memTotal + + ", memProcessed=" + memProcessed + + ", memRemaining=" + memRemaining + + ", fileTotal=" + fileTotal + + ", fileProcessed=" + fileProcessed + + ", fileRemaining=" + fileRemaining + + '}'; + } +} diff --git a/cosmic-model/src/main/java/com/cloud/legacymodel/communication/command/MigrationProgressCommand.java b/cosmic-model/src/main/java/com/cloud/legacymodel/communication/command/MigrationProgressCommand.java new file mode 100644 index 0000000000..45068c6543 --- /dev/null +++ b/cosmic-model/src/main/java/com/cloud/legacymodel/communication/command/MigrationProgressCommand.java @@ -0,0 +1,22 @@ +package com.cloud.legacymodel.communication.command; + +public class MigrationProgressCommand extends Command { + String vmName; + + protected MigrationProgressCommand() { + super(); + } + + public MigrationProgressCommand(final String vmName) { + this.vmName = vmName; + } + + public String getVmName() { + return vmName; + } + + @Override + public boolean executeInSequence() { + return false; + } +}