From 3b1ab6fed2c4d611709ec853a2d8d03c74eb6e10 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Fri, 5 Sep 2025 10:48:57 -0300 Subject: [PATCH 01/22] Add source VM name on virt-v2v migration log entries --- .../wrapper/LibvirtConvertInstanceCommandWrapper.java | 6 +++--- .../wrapper/LibvirtConvertInstanceCommandWrapperTest.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java index abc408f20f61..39953f0bbd3b 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapper.java @@ -116,7 +116,7 @@ public Answer execute(ConvertInstanceCommand cmd, LibvirtComputingResource serve boolean cleanupSecondaryStorage = false; try { - boolean result = performInstanceConversion(sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid, + boolean result = performInstanceConversion(sourceInstanceName, sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid, timeout, verboseModeEnabled); if (!result) { String err = String.format( @@ -217,7 +217,7 @@ private boolean exportOVAFromVMOnVcenter(String vmExportUrl, return exitValue == 0; } - protected boolean performInstanceConversion(String sourceOVFDirPath, + protected boolean performInstanceConversion(String sourceInstanceName, String sourceOVFDirPath, String temporaryConvertFolder, String temporaryConvertUuid, long timeout, boolean verboseModeEnabled) { @@ -233,7 +233,7 @@ protected boolean performInstanceConversion(String sourceOVFDirPath, script.add("-v"); } - String logPrefix = String.format("virt-v2v ovf source: %s progress", sourceOVFDirPath); + String logPrefix = String.format("(%s) virt-v2v ovf source: %s progress", sourceInstanceName, sourceOVFDirPath); OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); script.execute(outputLogger); int exitValue = script.getExitValue(); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java index b369cf25f3d0..068c8b1245c3 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtConvertInstanceCommandWrapperTest.java @@ -166,7 +166,7 @@ public void testExecuteConvertFailure() { Answer answer = convertInstanceCommandWrapper.execute(cmd, libvirtComputingResourceMock); Assert.assertFalse(answer.getResult()); - Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(), + Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean()); } } From e194ba96117f1d54f2f7b9e53788b4453d945162 Mon Sep 17 00:00:00 2001 From: nvazquez Date: Sun, 7 Sep 2025 18:20:14 -0300 Subject: [PATCH 02/22] Improve the feedback by displaying the running importing tasks --- .../admin/vm/ListImportVMTasksCmd.java | 109 +++++ .../api/response/ImportVMTaskResponse.java | 221 ++++++++++ .../apache/cloudstack/vm/VmImportService.java | 4 + .../java/com/cloud/vm/ImportVMTaskVO.java | 243 +++++++++++ .../com/cloud/vm/dao/ImportVMTaskDao.java | 29 ++ .../com/cloud/vm/dao/ImportVMTaskDaoImpl.java | 65 +++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42100to42200.sql | 27 ++ .../vm/UnmanagedVMsManagerImpl.java | 118 ++++++ tools/apidoc/gen_toc.py | 3 +- ui/src/views/tools/ManageInstances.vue | 379 +++++++++++------- 11 files changed, 1056 insertions(+), 143 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java new file mode 100644 index 000000000000..3a37fd85e179 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java @@ -0,0 +1,109 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package org.apache.cloudstack.api.command.admin.vm; + +import com.cloud.exception.ConcurrentOperationException; +import com.cloud.exception.InsufficientCapacityException; +import com.cloud.exception.NetworkRuleConflictException; +import com.cloud.exception.ResourceAllocationException; +import com.cloud.exception.ResourceUnavailableException; +import com.cloud.user.Account; +import org.apache.cloudstack.acl.RoleType; +import org.apache.cloudstack.api.APICommand; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseListCmd; +import org.apache.cloudstack.api.Parameter; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.response.AccountResponse; +import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.ImportVMTaskResponse; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.VmImportService; + +import javax.inject.Inject; + +@APICommand(name = "listImportVmTasks", + description = "List running import virtual machine tasks from a unmanaged hosts into CloudStack", + responseObject = ImportVMTaskResponse.class, + responseView = ResponseObject.ResponseView.Full, + requestHasSensitiveInfo = false, + authorized = {RoleType.Admin}, + since = "4.22.0") +public class ListImportVMTasksCmd extends BaseListCmd { + + @Inject + public VmImportService vmImportService; + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.UUID, + entityType = ZoneResponse.class, + required = true, + description = "the zone ID") + private Long zoneId; + + @Parameter(name = ApiConstants.ACCOUNT_ID, + type = CommandType.UUID, + entityType = AccountResponse.class, + description = "the ID of the Account") + private Long accountId; + + @Parameter(name = ApiConstants.VCENTER, + type = CommandType.STRING, + description = "The name/ip of vCenter. Make sure it is IP address or full qualified domain name for host running vCenter server.") + private String vcenter; + + @Parameter(name = ApiConstants.CONVERT_INSTANCE_HOST_ID, + type = CommandType.UUID, + entityType = HostResponse.class, + description = "Conversion host of the importing task") + private Long convertHostId; + + public Long getZoneId() { + return zoneId; + } + + public Long getAccountId() { + return accountId; + } + + public String getVcenter() { + return vcenter; + } + + public Long getConvertHostId() { + return convertHostId; + } + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + ListResponse response = vmImportService.listImportVMTasks(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Account account = CallContext.current().getCallingAccount(); + if (account != null) { + return account.getId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java new file mode 100644 index 000000000000..a47e13820b61 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java @@ -0,0 +1,221 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package org.apache.cloudstack.api.response; + +import com.cloud.serializer.Param; +import com.google.gson.annotations.SerializedName; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.BaseResponse; + +import java.util.Date; + +public class ImportVMTaskResponse extends BaseResponse { + + @SerializedName(ApiConstants.ID) + @Param(description = "the ID of importing task") + private String id; + + @SerializedName(ApiConstants.ZONE_ID) + @Param(description = "the Zone ID") + private String zoneId; + + @SerializedName(ApiConstants.ZONE_NAME) + @Param(description = "the Zone name") + private String zoneName; + + @SerializedName(ApiConstants.ACCOUNT) + @Param(description = "the account name") + private String accountName; + + @SerializedName(ApiConstants.ACCOUNT_ID) + @Param(description = "the ID of account") + private String accountId; + + @SerializedName(ApiConstants.DISPLAY_NAME) + @Param(description = "the display name of the importing VM") + private String displayName; + + @SerializedName(ApiConstants.VCENTER) + @Param(description = "the vcenter name of the importing VM task") + private String vcenter; + + @SerializedName(ApiConstants.DATACENTER_NAME) + @Param(description = "the datacenter name of the importing VM task") + private String datacenterName; + + @SerializedName("sourcevmname") + @Param(description = "the source VM name") + private String sourceVMName; + + @SerializedName("step") + @Param(description = "the current step on the importing VM task") + private String step; + + @SerializedName(ApiConstants.DURATION) + @Param(description = "the current step duration in seconds") + private Long duration; + + @SerializedName(ApiConstants.DESCRIPTION) + @Param(description = "the current step description on the importing VM task") + private String description; + + @SerializedName(ApiConstants.CONVERT_INSTANCE_HOST_ID) + @Param(description = "the ID of the host on which the instance is being converted") + private String convertInstanceHostId; + + @SerializedName("convertinstancehostname") + @Param(description = "the name of the host on which the instance is being converted") + private String convertInstanceHostName; + + @SerializedName(ApiConstants.CREATED) + @Param(description = "the create date of the importing task") + private Date created; + + @SerializedName(ApiConstants.LAST_UPDATED) + @Param(description = "the last updated date of the importing task") + private Date lastUpdated; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getZoneId() { + return zoneId; + } + + public void setZoneId(String zoneId) { + this.zoneId = zoneId; + } + + public String getZoneName() { + return zoneName; + } + + public void setZoneName(String zoneName) { + this.zoneName = zoneName; + } + + public String getAccountName() { + return accountName; + } + + public void setAccountName(String accountName) { + this.accountName = accountName; + } + + public String getAccountId() { + return accountId; + } + + public void setAccountId(String accountId) { + this.accountId = accountId; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getVcenter() { + return vcenter; + } + + public void setVcenter(String vcenter) { + this.vcenter = vcenter; + } + + public String getDatacenterName() { + return datacenterName; + } + + public void setDatacenterName(String datacenterName) { + this.datacenterName = datacenterName; + } + + public String getSourceVMName() { + return sourceVMName; + } + + public void setSourceVMName(String sourceVMName) { + this.sourceVMName = sourceVMName; + } + + public String getStep() { + return step; + } + + public void setStep(String step) { + this.step = step; + } + + public Long getDuration() { + return duration; + } + + public void setDuration(Long duration) { + this.duration = duration; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getConvertInstanceHostId() { + return convertInstanceHostId; + } + + public void setConvertInstanceHostId(String convertInstanceHostId) { + this.convertInstanceHostId = convertInstanceHostId; + } + + public String getConvertInstanceHostName() { + return convertInstanceHostName; + } + + public void setConvertInstanceHostName(String convertInstanceHostName) { + this.convertInstanceHostName = convertInstanceHostName; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastUpdated() { + return lastUpdated; + } + + public void setLastUpdated(Date lastUpdated) { + this.lastUpdated = lastUpdated; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java index 04ef248fb8ac..bcbf5b907d87 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java @@ -19,8 +19,10 @@ import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; import org.apache.cloudstack.api.command.admin.vm.ImportVmCmd; +import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd; import org.apache.cloudstack.api.command.admin.vm.ListUnmanagedInstancesCmd; import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; +import org.apache.cloudstack.api.response.ImportVMTaskResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -42,4 +44,6 @@ public String toString() { UserVmResponse importVm(ImportVmCmd cmd); ListResponse listVmsForImport(ListVmsForImportCmd cmd); + + ListResponse listImportVMTasks(ListImportVMTasksCmd cmd); } diff --git a/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java b/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java new file mode 100644 index 000000000000..71dfa785f14d --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java @@ -0,0 +1,243 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package com.cloud.vm; + +import org.apache.cloudstack.api.Identity; +import org.apache.cloudstack.api.InternalIdentity; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; +import java.util.Date; +import java.util.UUID; + +@Entity +@Table(name = "import_vm_task") +public class ImportVMTaskVO implements Identity, InternalIdentity { + + public enum Step { + Prepare, CloningInstance, ConvertingInstance, Importing, Cleaning, Completed + } + + public ImportVMTaskVO(long zoneId, long accountId, long userId, String displayName, + String vcenter, String datacenter, String sourceVMName, long convertHostId, long importHostId) { + this.zoneId = zoneId; + this.accountId = accountId; + this.userId = userId; + this.displayName = displayName; + this.vcenter = vcenter; + this.datacenter = datacenter; + this.sourceVMName = sourceVMName; + this.step = Step.Prepare; + this.uuid = UUID.randomUUID().toString(); + this.convertHostId = convertHostId; + this.importHostId = importHostId; + } + + public ImportVMTaskVO() { + } + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private long id; + + @Column(name = "uuid") + private String uuid; + + @Column(name = "zone_id") + private long zoneId; + + @Column(name = "account_id") + private long accountId; + + @Column(name = "user_id") + private long userId; + + @Column(name = "display_name") + private String displayName; + + @Column(name = "vcenter") + private String vcenter; + + @Column(name = "datacenter") + private String datacenter; + + @Column(name = "source_vm_name") + private String sourceVMName; + + @Column(name = "convert_host_id") + private long convertHostId; + + @Column(name = "import_host_id") + private long importHostId; + + @Column(name = "step") + Step step; + + @Column(name = "description") + private String description; + + @Column(name = "created") + @Temporal(value = TemporalType.TIMESTAMP) + private Date created; + + @Column(name = "updated") + @Temporal(value = TemporalType.TIMESTAMP) + private Date updated; + + @Column(name = "removed") + @Temporal(value = TemporalType.TIMESTAMP) + private Date removed; + + @Override + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Override + public String getUuid() { + return uuid; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public long getZoneId() { + return zoneId; + } + + public void setZoneId(long zoneId) { + this.zoneId = zoneId; + } + + public long getAccountId() { + return accountId; + } + + public void setAccountId(long accountId) { + this.accountId = accountId; + } + + public long getUserId() { + return userId; + } + + public void setUserId(long userId) { + this.userId = userId; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getVcenter() { + return vcenter; + } + + public void setVcenter(String vcenter) { + this.vcenter = vcenter; + } + + public String getDatacenter() { + return datacenter; + } + + public void setDatacenter(String datacenter) { + this.datacenter = datacenter; + } + + public String getSourceVMName() { + return sourceVMName; + } + + public void setSourceVMName(String sourceVMName) { + this.sourceVMName = sourceVMName; + } + + public long getConvertHostId() { + return convertHostId; + } + + public void setConvertHostId(long convertHostId) { + this.convertHostId = convertHostId; + } + + public long getImportHostId() { + return importHostId; + } + + public void setImportHostId(long importHostId) { + this.importHostId = importHostId; + } + + public Step getStep() { + return step; + } + + public void setStep(Step step) { + this.step = step; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getUpdated() { + return updated; + } + + public void setUpdated(Date updated) { + this.updated = updated; + } + + public Date getRemoved() { + return removed; + } + + public void setRemoved(Date removed) { + this.removed = removed; + } +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java new file mode 100644 index 000000000000..37c614a6c3ed --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java @@ -0,0 +1,29 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// +package com.cloud.vm.dao; + +import com.cloud.utils.db.GenericDao; +import com.cloud.vm.ImportVMTaskVO; + +import java.util.List; + +public interface ImportVMTaskDao extends GenericDao { + + List listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId); +} diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java new file mode 100644 index 000000000000..db2f48e9ef5f --- /dev/null +++ b/engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java @@ -0,0 +1,65 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +package com.cloud.vm.dao; + +import com.cloud.utils.db.GenericDaoBase; +import com.cloud.utils.db.SearchBuilder; +import com.cloud.utils.db.SearchCriteria; +import com.cloud.vm.ImportVMTaskVO; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import java.util.List; + +@Component +public class ImportVMTaskDaoImpl extends GenericDaoBase implements ImportVMTaskDao { + + private SearchBuilder AllFieldsSearch; + + public ImportVMTaskDaoImpl() { + } + + @PostConstruct + void init() { + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and("zoneId", AllFieldsSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("vcenter", AllFieldsSearch.entity().getVcenter(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("convertHostId", AllFieldsSearch.entity().getConvertHostId(), SearchCriteria.Op.EQ); + AllFieldsSearch.done(); + } + + + @Override + public List listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId) { + SearchCriteria sc = AllFieldsSearch.create(); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (StringUtils.isNotBlank(vcenter)) { + sc.setParameters("vcenter", vcenter); + } + if (convertHostId != null) { + sc.setParameters("convertHostId", convertHostId); + } + return listBy(sc); + } +} diff --git a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml index 7e9d6c3b54f0..0656d5e3c440 100644 --- a/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml +++ b/engine/schema/src/main/resources/META-INF/cloudstack/core/spring-engine-schema-core-daos-context.xml @@ -309,4 +309,5 @@ + diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql index cf3fe2ed7726..dd0462e36042 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42100to42200.sql @@ -21,3 +21,30 @@ -- Increase length of scripts_version column to 128 due to md5sum to sha512sum change CALL `cloud`.`IDEMPOTENT_CHANGE_COLUMN`('cloud.domain_router', 'scripts_version', 'scripts_version', 'VARCHAR(128)'); + +-- VMware to KVM migration improvements +CREATE TABLE IF NOT EXISTS `cloud`.`import_vm_task`( + `id` bigint unsigned NOT NULL auto_increment COMMENT 'id', + `uuid` varchar(40), + `zone_id` bigint unsigned NOT NULL COMMENT 'Zone ID', + `account_id` bigint unsigned NOT NULL COMMENT 'Account ID', + `user_id` bigint unsigned NOT NULL COMMENT 'User ID', + `display_name` varchar(255) COMMENT 'Display VM Name', + `vcenter` varchar(255) COMMENT 'VCenter', + `datacenter` varchar(255) COMMENT 'VCenter Datacenter name', + `source_vm_name` varchar(255) COMMENT 'Source VM name on vCenter', + `convert_host_id` bigint unsigned COMMENT 'Convert Host ID', + `import_host_id` bigint unsigned COMMENT 'Import Host ID', + `step` varchar(20) NOT NULL COMMENT 'Importing VM Task Step', + `description` varchar(255) COMMENT 'Importing VM Task Description', + `created` datetime NOT NULL COMMENT 'date created', + `updated` datetime COMMENT 'date updated if not null', + `removed` datetime COMMENT 'date removed if not null', + PRIMARY KEY (`id`), + CONSTRAINT `fk_import_vm_task__zone_id` FOREIGN KEY `fk_import_vm_task__zone_id` (`zone_id`) REFERENCES `data_center`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_import_vm_task__account_id` FOREIGN KEY `fk_import_vm_task__account_id` (`account_id`) REFERENCES `account`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_import_vm_task__user_id` FOREIGN KEY `fk_import_vm_task__user_id` (`user_id`) REFERENCES `user`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_import_vm_task__convert_host_id` FOREIGN KEY `fk_import_vm_task__convert_host_id` (`convert_host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + CONSTRAINT `fk_import_vm_task__import_host_id` FOREIGN KEY `fk_import_vm_task__import_host_id` (`import_host_id`) REFERENCES `host`(`id`) ON DELETE CASCADE, + INDEX `i_import_vm_task__zone_id`(`zone_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index ad3d2a0f0ec2..f600bf2d7a7f 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -120,12 +120,14 @@ import com.cloud.user.UserVO; import com.cloud.user.dao.UserDao; import com.cloud.uservm.UserVm; +import com.cloud.utils.DateUtil; import com.cloud.utils.LogUtils; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; import com.cloud.vm.DiskProfile; +import com.cloud.vm.ImportVMTaskVO; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; @@ -137,6 +139,7 @@ import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.VmDetailConstants; +import com.cloud.vm.dao.ImportVMTaskDao; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; import com.cloud.vm.dao.VMInstanceDetailsDao; @@ -151,9 +154,11 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; import org.apache.cloudstack.api.command.admin.vm.ImportVmCmd; +import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd; import org.apache.cloudstack.api.command.admin.vm.ListUnmanagedInstancesCmd; import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; import org.apache.cloudstack.api.command.admin.vm.UnmanageVMInstanceCmd; +import org.apache.cloudstack.api.response.ImportVMTaskResponse; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.NicResponse; import org.apache.cloudstack.api.response.UnmanagedInstanceDiskResponse; @@ -179,16 +184,24 @@ import org.apache.logging.log4j.Logger; import javax.inject.Inject; +import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; +import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; +import static com.cloud.vm.ImportVMTaskVO.Step.CloningInstance; +import static com.cloud.vm.ImportVMTaskVO.Step.Completed; +import static com.cloud.vm.ImportVMTaskVO.Step.ConvertingInstance; +import static com.cloud.vm.ImportVMTaskVO.Step.Importing; import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS; import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS; @@ -283,6 +296,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { private ImageStoreDao imageStoreDao; @Inject private DataStoreManager dataStoreManager; + @Inject + private ImportVMTaskDao importVMTaskDao; protected Gson gson; @@ -1606,6 +1621,48 @@ private String createOvfTemplateOfSourceVmwareUnmanagedInstance(String vcenter, return vmwareGuru.createVMTemplateOutOfBand(sourceHostName, sourceVMwareInstanceName, params, convertLocation, threadsCountToExportOvf); } + protected ImportVMTaskVO createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName, + String vcenter, String datacenterName, String sourceVMName, + HostVO convertHost, HostVO importHost) { + logger.debug("Creating import VM task entry for VM: {} for account {} on zone {} " + + "from the vCenter: {} / datacenter: {} / source VM: {}", + sourceVMName, owner.getAccountName(), zone.getName(), displayName, vcenter, datacenterName); + ImportVMTaskVO importVMTaskVO = new ImportVMTaskVO(zone.getId(), owner.getAccountId(), userId, displayName, + vcenter, datacenterName, sourceVMName, convertHost.getId(), importHost.getId()); + return importVMTaskDao.persist(importVMTaskVO); + } + + private String getStepDescription(ImportVMTaskVO importVMTaskVO, HostVO convertHost, HostVO importHost, + ImportVMTaskVO.Step step, Date updatedDate) { + String sourceVMName = importVMTaskVO.getSourceVMName(); + String vcenter = importVMTaskVO.getVcenter(); + String datacenter = importVMTaskVO.getDatacenter(); + + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(String.format("[%s] ", DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate))); + if (CloningInstance == step) { + stringBuilder.append(String.format("Cloning source instance: %s on vCenter: %s / datacenter: %s", sourceVMName, vcenter, datacenter)); + } else if (ConvertingInstance == step) { + stringBuilder.append(String.format("Converting the cloned VMware instance to a KVM instance on the host: %s", convertHost.getName())); + } else if (Importing == step) { + stringBuilder.append(String.format("Importing the converted KVM instance on the host: %s", importHost.getName())); + } + return stringBuilder.toString(); + } + + protected void updateImportVMTaskStep(ImportVMTaskVO importVMTaskVO, DataCenter zone, Account owner, HostVO convertHost, HostVO importHost, ImportVMTaskVO.Step step) { + logger.debug("Updating import VM task entry for VM: {} for account {} on zone {} " + + "from the vCenter: {} / datacenter: {} / source VM: {} to step: {}", + importVMTaskVO.getSourceVMName(), owner.getAccountName(), zone.getName(), importVMTaskVO.getDisplayName(), + importVMTaskVO.getVcenter(), importVMTaskVO.getDatacenter(), step); + Date updatedDate = DateUtil.now(); + String description = getStepDescription(importVMTaskVO, convertHost, importHost, step, updatedDate); + importVMTaskVO.setStep(step); + importVMTaskVO.setDescription(description); + importVMTaskVO.setUpdated(updatedDate); + importVMTaskDao.update(importVMTaskVO.getId(), importVMTaskVO); + } + protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster destinationCluster, VMTemplateVO template, String sourceVMName, String displayName, String hostName, Account caller, Account owner, long userId, @@ -1649,6 +1706,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster UnmanagedInstanceTO sourceVMwareInstance = null; DataStoreTO temporaryConvertLocation = null; String ovfTemplateOnConvertLocation = null; + ImportVMTaskVO importVMTaskVO = null; try { HostVO convertHost = selectKVMHostForConversionInCluster(destinationCluster, convertInstanceHostId); HostVO importHost = selectKVMHostForImportingInCluster(destinationCluster, importInstanceHostId); @@ -1660,6 +1718,9 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster destinationCluster, convertHost, convertStoragePoolId); List convertStoragePools = findInstanceConversionStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap); long importStartTime = System.currentTimeMillis(); + importVMTaskVO = createImportVMTaskRecord(zone, owner, userId, displayName, vcenter, datacenterName, sourceVMName, + convertHost, importHost); + updateImportVMTaskStep(importVMTaskVO, zone, owner, convertHost, importHost, CloningInstance); Pair sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName); sourceVMwareInstance = sourceInstanceDetails.first(); isClonedInstance = sourceInstanceDetails.second(); @@ -1674,6 +1735,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster if (cmd.getForceMsToImportVmFiles() || !conversionSupportAnswer.isOvfExportSupported()) { // Uses MS for OVF export to temporary conversion location int noOfThreads = UnmanagedVMsManager.ThreadsOnMSToImportVMwareVMFiles.value(); + updateImportVMTaskStep(importVMTaskVO, zone, owner, convertHost, importHost, ConvertingInstance); ovfTemplateOnConvertLocation = createOvfTemplateOfSourceVmwareUnmanagedInstance( vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMwareInstance.getName(), temporaryConvertLocation, noOfThreads); @@ -1683,6 +1745,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster ovfTemplateOnConvertLocation); } else { // Uses KVM Host for OVF export to temporary conversion location, through ovftool + updateImportVMTaskStep(importVMTaskVO, zone, owner, convertHost, importHost, ConvertingInstance); convertedInstance = convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation( sourceVMName, sourceVMwareInstance, convertHost, importHost, convertStoragePools, serviceOffering, dataDiskOfferingMap, @@ -1690,6 +1753,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster } sanitizeConvertedInstance(convertedInstance, sourceVMwareInstance); + updateImportVMTaskStep(importVMTaskVO, zone, owner, convertHost, importHost, Importing); UserVm userVm = importVirtualMachineInternal(convertedInstance, instanceName, zone, destinationCluster, null, template, displayName, hostName, caller, owner, userId, serviceOffering, dataDiskOfferingMap, @@ -1698,6 +1762,8 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster long timeElapsedInSecs = (System.currentTimeMillis() - importStartTime) / 1000; logger.debug(String.format("VMware VM %s imported successfully to CloudStack instance %s (%s), Time taken: %d secs, OVF files imported from %s, Source VMware VM details - OS: %s, PowerState: %s, Disks: %s, NICs: %s", sourceVMName, instanceName, displayName, timeElapsedInSecs, (ovfTemplateOnConvertLocation != null)? "MS" : "KVM Host", sourceVMwareInstance.getOperatingSystem(), sourceVMwareInstance.getPowerState(), sourceVMwareInstance.getDisks(), sourceVMwareInstance.getNics())); + updateImportVMTaskStep(importVMTaskVO, zone, owner, convertHost, importHost, Completed); + importVMTaskDao.remove(importVMTaskVO.getId()); return userVm; } catch (CloudRuntimeException e) { logger.error(String.format("Error importing VM: %s", e.getMessage()), e); @@ -2197,6 +2263,7 @@ public List> getCommands() { cmdList.add(UnmanageVMInstanceCmd.class); cmdList.add(ListVmsForImportCmd.class); cmdList.add(ImportVmCmd.class); + cmdList.add(ListImportVMTasksCmd.class); return cmdList; } @@ -2852,6 +2919,57 @@ public ListResponse listVmsForImport(ListVmsForImport return listResponses; } + @Override + public ListResponse listImportVMTasks(ListImportVMTasksCmd cmd) { + Long zoneId = cmd.getZoneId(); + Long accountId = cmd.getAccountId(); + String vcenter = cmd.getVcenter(); + Long convertHostId = cmd.getConvertHostId(); + List tasks = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId); + List responses = new ArrayList<>(); + for (ImportVMTaskVO task : tasks) { + responses.add(createImportVMTaskResponse(task)); + } + ListResponse listResponses = new ListResponse<>(); + listResponses.setResponses(responses, responses.size()); + return listResponses; + } + + private ImportVMTaskResponse createImportVMTaskResponse(ImportVMTaskVO task) { + ImportVMTaskResponse response = new ImportVMTaskResponse(); + DataCenterVO zone = dataCenterDao.findById(task.getZoneId()); + if (zone != null) { + response.setZoneId(zone.getUuid()); + response.setZoneName(zone.getName()); + } + Account account = accountService.getAccount(task.getAccountId()); + if (account != null) { + response.setAccountId(account.getUuid()); + response.setAccountName(account.getAccountName()); + } + response.setVcenter(task.getVcenter()); + response.setDatacenterName(task.getDatacenter()); + response.setSourceVMName(task.getSourceVMName()); + response.setStep(task.getStep().name()); + Date updated = task.getUpdated(); + if (updated != null) { + Date currentDate = new Date(); + Duration duration = Duration.between(updated.toInstant(), currentDate.toInstant()); + long durationSeconds = TimeUnit.SECONDS.convert(duration.toMillis(), TimeUnit.MILLISECONDS); + response.setDuration(durationSeconds); + } + response.setDescription(task.getDescription()); + HostVO host = hostDao.findById(task.getConvertHostId()); + if (host != null) { + response.setConvertInstanceHostId(host.getUuid()); + response.setConvertInstanceHostName(host.getName()); + } + response.setCreated(task.getCreated()); + response.setLastUpdated(task.getUpdated()); + response.setObjectName("importvmtask"); + return response; + } + private HashMap getRemoteVmsOnKVMHost(long zoneId, String remoteHostUrl, String username, String password) { //ToDo: add option to list one Vm by name List hosts = resourceManager.listAllUpAndEnabledHostsInOneZoneByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index ad72b4c79381..e41a04ff2e1b 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -272,7 +272,8 @@ 'Extension' : 'Extension', 'Extensions' : 'Extension', 'CustomAction' : 'Extension', - 'CustomActions' : 'Extension' + 'CustomActions' : 'Extension', + 'ImportVmTask': 'Import VM Task' } diff --git a/ui/src/views/tools/ManageInstances.vue b/ui/src/views/tools/ManageInstances.vue index b6f3c50e2674..4a6ab296ca12 100644 --- a/ui/src/views/tools/ManageInstances.vue +++ b/ui/src/views/tools/ManageInstances.vue @@ -333,166 +333,199 @@ - - - - - -