You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by nv...@apache.org on 2025/10/10 23:00:34 UTC
(cloudstack) branch main updated: VMware to KVM Migrations improvements (#11594)
This is an automated email from the ASF dual-hosted git repository.
nvazquez pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cloudstack.git
The following commit(s) were added to refs/heads/main by this push:
new b106d6e190b VMware to KVM Migrations improvements (#11594)
b106d6e190b is described below
commit b106d6e190b098ee1e97264952ed59a277e5c360
Author: Nicolas Vazquez <ni...@gmail.com>
AuthorDate: Fri Oct 10 20:00:29 2025 -0300
VMware to KVM Migrations improvements (#11594)
* Add source VM name on virt-v2v migration log entries
* Improve the feedback by displaying the running importing tasks
* Add source VM name prefix on more conversion logs
* Improve listing and also list completed tasks
* Pass extra parameters to virt-v2v if administrator allows via global setting
* Add Force converting directly to storage pool option
* Refactor based on review comments
* Add properties for env vars for the instance conversion
* Add separate component for Import VM Tasks
* applying copilot suggestions from code review
Co-authored-by: Copilot <17...@users.noreply.github.com>
* Fix importing unmanaged instances due to incorrect internal name
* Add VM prefix on each log operation for conversion
* Log the original VM name instead of the cloned VM in case of cloning
* Allow searching storage pool by UUID after conversion to support SharedMountPoint
* Fix search pools logic
* Improve UI and add checks for force convert to pool parameter
* Support Local storage when forceconverttopool is set to true
* Add config key to for allowed extra params and add validation
* Fix params lists
* Fix compile error
* Remove extra stubbings
* Fix extra params execution
---------
Co-authored-by: Abhishek Kumar <ab...@gmail.com>
Co-authored-by: Copilot <17...@users.noreply.github.com>
Co-authored-by: Suresh Kumar Anaparti <su...@gmail.com>
---
agent/conf/agent.properties | 6 +
.../cloud/agent/properties/AgentProperties.java | 14 +
.../org/apache/cloudstack/api/ApiConstants.java | 4 +
.../api/command/admin/vm/ImportVmCmd.java | 20 ++
.../api/command/admin/vm/ListImportVMTasksCmd.java | 123 +++++++
.../api/response/ImportVMTaskResponse.java | 245 ++++++++++++++
.../org/apache/cloudstack/vm/ImportVmTask.java | 28 ++
.../apache/cloudstack/vm/ImportVmTasksManager.java | 38 +++
.../cloud/agent/api/ConvertInstanceCommand.java | 17 +-
.../agent/api/ImportConvertedInstanceCommand.java | 9 +-
.../src/main/java/com/cloud/vm/ImportVMTaskVO.java | 259 +++++++++++++++
.../java/com/cloud/vm/dao/ImportVMTaskDao.java | 29 ++
.../java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java | 65 ++++
.../spring-engine-schema-core-daos-context.xml | 1 +
.../resources/META-INF/db/schema-42100to42200.sql | 30 ++
.../kvm/resource/LibvirtComputingResource.java | 26 ++
.../LibvirtConvertInstanceCommandWrapper.java | 73 +++--
...bvirtImportConvertedInstanceCommandWrapper.java | 16 +-
.../LibvirtConvertInstanceCommandWrapperTest.java | 23 +-
.../cloudstack/vm/ImportVmTasksManagerImpl.java | 225 +++++++++++++
.../cloudstack/vm/UnmanagedVMsManagerImpl.java | 276 ++++++++++++----
.../spring-server-compute-context.xml | 2 +
.../cloudstack/vm/UnmanagedVMsManagerImplTest.java | 104 +++++-
tools/apidoc/gen_toc.py | 3 +-
ui/public/locales/en.json | 5 +-
ui/src/views/tools/ImportUnmanagedInstance.vue | 75 ++++-
ui/src/views/tools/ImportVmTasks.vue | 150 +++++++++
ui/src/views/tools/ManageInstances.vue | 354 ++++++++++++---------
.../main/java/com/cloud/utils/script/Script.java | 1 +
29 files changed, 1945 insertions(+), 276 deletions(-)
diff --git a/agent/conf/agent.properties b/agent/conf/agent.properties
index 2a083de6fe5..0dc5b8211e0 100644
--- a/agent/conf/agent.properties
+++ b/agent/conf/agent.properties
@@ -451,3 +451,9 @@ iscsi.session.cleanup.enabled=false
# If set to true, creates VMs as full clones of their templates on KVM hypervisor. Creates as linked clones otherwise.
# create.full.clone=false
+
+# Instance conversion TMPDIR env var
+#convert.instance.env.tmpdir=
+
+# Instance conversion VIRT_V2V_TMPDIR env var
+#convert.instance.env.virtv2v.tmpdir=
diff --git a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
index 57dc607b920..1561f0d5cfb 100644
--- a/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
+++ b/agent/src/main/java/com/cloud/agent/properties/AgentProperties.java
@@ -794,6 +794,20 @@ public class AgentProperties{
*/
public static final Property<Boolean> VIRTV2V_VERBOSE_ENABLED = new Property<>("virtv2v.verbose.enabled", false);
+ /**
+ * Set env TMPDIR var for virt-v2v Instance Conversion from VMware to KVM
+ * Data type: String.<br>
+ * Default value: <code>null</code>
+ */
+ public static final Property<String> CONVERT_ENV_TMPDIR = new Property<>("convert.instance.env.tmpdir", null, String.class);
+
+ /**
+ * Set env VIRT_V2V_TMPDIR var for virt-v2v Instance Conversion from VMware to KVM
+ * Data type: String.<br>
+ * Default value: <code>null</code>
+ */
+ public static final Property<String> CONVERT_ENV_VIRTV2V_TMPDIR = new Property<>("convert.instance.env.virtv2v.tmpdir", null, String.class);
+
/**
* BGP controll CIDR
* Data type: String.<br>
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 475b845af9b..27b17d96b8f 100644
--- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
+++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java
@@ -225,6 +225,7 @@ public class ApiConstants {
public static final String EVENT_TYPE = "eventtype";
public static final String EXPIRES = "expires";
public static final String EXTRA_CONFIG = "extraconfig";
+ public static final String EXTRA_PARAMS = "extraparams";
public static final String EXTRA_DHCP_OPTION = "extradhcpoption";
public static final String EXTRA_DHCP_OPTION_NAME = "extradhcpoptionname";
public static final String EXTRA_DHCP_OPTION_CODE = "extradhcpoptioncode";
@@ -243,6 +244,8 @@ public class ApiConstants {
public static final String FIRSTNAME = "firstname";
public static final String FORCED = "forced";
public static final String FORCED_DESTROY_LOCAL_STORAGE = "forcedestroylocalstorage";
+ public static final String FORCE_CONVERT_TO_POOL = "forceconverttopool";
+
public static final String FORCE_DELETE_HOST = "forcedeletehost";
public static final String FORCE_MS_TO_IMPORT_VM_FILES = "forcemstoimportvmfiles";
public static final String FORCE_UPDATE_OS_TYPE = "forceupdateostype";
@@ -529,6 +532,7 @@ public class ApiConstants {
public static final String SHOW_CAPACITIES = "showcapacities";
public static final String SHOW_REMOVED = "showremoved";
public static final String SHOW_RESOURCE_ICON = "showicon";
+ public static final String SHOW_COMPLETED = "showcompleted";
public static final String SHOW_INACTIVE = "showinactive";
public static final String SHOW_UNIQUE = "showunique";
public static final String SIGNATURE = "signature";
diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
index db43b53ab9a..f7940460d6c 100644
--- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java
@@ -159,6 +159,18 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
description = "(only for importing VMs from VMware to KVM) optional - if true, forces MS to export OVF from VMware to temporary storage, else uses KVM Host if ovftool is available, falls back to MS if not.")
private Boolean forceMsToImportVmFiles;
+ @Parameter(name = ApiConstants.EXTRA_PARAMS,
+ type = CommandType.STRING,
+ since = "4.22",
+ description = "(only for importing VMs from VMware to KVM) optional - extra parameters to be passed on the virt-v2v command, if allowed by the administrator")
+ private String extraParams;
+
+ @Parameter(name = ApiConstants.FORCE_CONVERT_TO_POOL,
+ type = CommandType.BOOLEAN,
+ since = "4.22",
+ description = "(only for importing VMs from VMware to KVM) optional - if true, forces virt-v2v conversions to write directly on the provided storage pool (avoid using temporary conversion pool).")
+ private Boolean forceConvertToPool;
+
/////////////////////////////////////////////////////
/////////////////// Accessors ///////////////////////
/////////////////////////////////////////////////////
@@ -248,6 +260,14 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
return EventTypes.EVENT_VM_IMPORT;
}
+ public String getExtraParams() {
+ return extraParams;
+ }
+
+ public boolean getForceConvertToPool() {
+ return BooleanUtils.toBooleanDefaultIfNull(forceConvertToPool, false);
+ }
+
@Override
public String getEventDescription() {
String vmName = getName();
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 00000000000..a0c8cb57ee6
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java
@@ -0,0 +1,123 @@
+// 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.ImportVmTasksManager;
+
+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")
+public class ListImportVMTasksCmd extends BaseListCmd {
+
+ @Inject
+ public ImportVmTasksManager importVmTasksManager;
+
+ @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;
+
+ @Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "Whether to list all import tasks.")
+ private boolean listAll = false;
+
+ @Parameter(name = ApiConstants.SHOW_COMPLETED, type = CommandType.BOOLEAN, description = "Whether to list completed tasks.")
+ private boolean showCompleted = false;
+
+ public Long getZoneId() {
+ return zoneId;
+ }
+
+ public Long getAccountId() {
+ return accountId;
+ }
+
+ public String getVcenter() {
+ return vcenter;
+ }
+
+ public Long getConvertHostId() {
+ return convertHostId;
+ }
+
+ public boolean isListAll() {
+ return listAll;
+ }
+
+ public boolean isShowCompleted() {
+ return showCompleted;
+ }
+
+ @Override
+ public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
+ ListResponse<ImportVMTaskResponse> response = importVmTasksManager.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 00000000000..dd49127f1bd
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java
@@ -0,0 +1,245 @@
+//
+// 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.VIRTUAL_MACHINE_ID)
+ @Param(description = "the ID of the imported VM (after task is completed)")
+ private String virtualMachineId;
+
+ @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("stepduration")
+ @Param(description = "the duration of the current step")
+ private String stepDuration;
+
+ @SerializedName(ApiConstants.DURATION)
+ @Param(description = "the total task duration")
+ private String 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 getVirtualMachineId() {
+ return virtualMachineId;
+ }
+
+ public void setVirtualMachineId(String virtualMachineId) {
+ this.virtualMachineId = virtualMachineId;
+ }
+
+ 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 String getStepDuration() {
+ return stepDuration;
+ }
+
+ public void setStepDuration(String stepDuration) {
+ this.stepDuration = stepDuration;
+ }
+
+ public String getDuration() {
+ return duration;
+ }
+
+ public void setDuration(String 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/ImportVmTask.java b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java
new file mode 100644
index 00000000000..96d7cd346f3
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java
@@ -0,0 +1,28 @@
+/*
+ * 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.vm;
+
+import org.apache.cloudstack.api.Identity;
+import org.apache.cloudstack.api.InternalIdentity;
+
+public interface ImportVmTask extends Identity, InternalIdentity {
+ enum Step {
+ Prepare, CloningInstance, ConvertingInstance, Importing, Cleaning, Completed
+ }
+}
diff --git a/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java
new file mode 100644
index 00000000000..44eed8c531a
--- /dev/null
+++ b/api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java
@@ -0,0 +1,38 @@
+// 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.vm;
+
+import com.cloud.dc.DataCenter;
+import com.cloud.host.Host;
+import com.cloud.user.Account;
+import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd;
+import org.apache.cloudstack.api.response.ImportVMTaskResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+
+public interface ImportVmTasksManager {
+
+ ListResponse<ImportVMTaskResponse> listImportVMTasks(ListImportVMTasksCmd cmd);
+
+ ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName,
+ String vcenter, String datacenterName, String sourceVMName,
+ Host convertHost, Host importHost);
+
+ void updateImportVMTaskStep(ImportVmTask importVMTaskVO, DataCenter zone, Account owner, Host convertHost,
+ Host importHost, Long vmId, ImportVmTask.Step step);
+
+ boolean removeImportVMTask(long taskId);
+}
diff --git a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
index f938d0ac55d..24336747ccf 100644
--- a/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/ConvertInstanceCommand.java
@@ -23,30 +23,37 @@ import com.cloud.hypervisor.Hypervisor;
public class ConvertInstanceCommand extends Command {
private RemoteInstanceTO sourceInstance;
+ private String originalVMName;
private Hypervisor.HypervisorType destinationHypervisorType;
private DataStoreTO conversionTemporaryLocation;
private String templateDirOnConversionLocation;
private boolean checkConversionSupport;
private boolean exportOvfToConversionLocation;
private int threadsCountToExportOvf = 0;
+ private String extraParams;
public ConvertInstanceCommand() {
}
public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, DataStoreTO conversionTemporaryLocation,
- String templateDirOnConversionLocation, boolean checkConversionSupport, boolean exportOvfToConversionLocation) {
+ String templateDirOnConversionLocation, boolean checkConversionSupport, boolean exportOvfToConversionLocation, String sourceVMName) {
this.sourceInstance = sourceInstance;
this.destinationHypervisorType = destinationHypervisorType;
this.conversionTemporaryLocation = conversionTemporaryLocation;
this.templateDirOnConversionLocation = templateDirOnConversionLocation;
this.checkConversionSupport = checkConversionSupport;
this.exportOvfToConversionLocation = exportOvfToConversionLocation;
+ this.originalVMName = sourceVMName;
}
public RemoteInstanceTO getSourceInstance() {
return sourceInstance;
}
+ public String getOriginalVMName() {
+ return originalVMName;
+ }
+
public Hypervisor.HypervisorType getDestinationHypervisorType() {
return destinationHypervisorType;
}
@@ -75,6 +82,14 @@ public class ConvertInstanceCommand extends Command {
this.threadsCountToExportOvf = threadsCountToExportOvf;
}
+ public String getExtraParams() {
+ return extraParams;
+ }
+
+ public void setExtraParams(String extraParams) {
+ this.extraParams = extraParams;
+ }
+
@Override
public boolean executeInSequence() {
return false;
diff --git a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
index 9d50e852ced..eadfa6556f8 100644
--- a/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
+++ b/core/src/main/java/com/cloud/agent/api/ImportConvertedInstanceCommand.java
@@ -27,17 +27,20 @@ public class ImportConvertedInstanceCommand extends Command {
private List<String> destinationStoragePools;
private DataStoreTO conversionTemporaryLocation;
private String temporaryConvertUuid;
+ private boolean forceConvertToPool;
public ImportConvertedInstanceCommand() {
}
public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance,
List<String> destinationStoragePools,
- DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid) {
+ DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid,
+ boolean forceConvertToPool) {
this.sourceInstance = sourceInstance;
this.destinationStoragePools = destinationStoragePools;
this.conversionTemporaryLocation = conversionTemporaryLocation;
this.temporaryConvertUuid = temporaryConvertUuid;
+ this.forceConvertToPool = forceConvertToPool;
}
public RemoteInstanceTO getSourceInstance() {
@@ -56,6 +59,10 @@ public class ImportConvertedInstanceCommand extends Command {
return temporaryConvertUuid;
}
+ public boolean isForceConvertToPool() {
+ return forceConvertToPool;
+ }
+
@Override
public boolean executeInSequence() {
return false;
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 00000000000..e472b56460a
--- /dev/null
+++ b/engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java
@@ -0,0 +1,259 @@
+//
+// 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.vm.ImportVmTask;
+
+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 ImportVmTask {
+
+ 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 = "vm_id")
+ private Long vmId;
+ @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")
+ private Step step;
+
+ @Column(name = "description")
+ private String description;
+
+ @Column(name = "duration")
+ private Long duration;
+
+ @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 Long getVmId() {
+ return vmId;
+ }
+
+ public void setVmId(Long vmId) {
+ this.vmId = vmId;
+ }
+
+ 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 Long getDuration() {
+ return duration;
+ }
+
+ public void setDuration(Long duration) {
+ this.duration = duration;
+ }
+
+ 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 00000000000..990dacb33fb
--- /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<ImportVMTaskVO, Long> {
+
+ List<ImportVMTaskVO> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted);
+}
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 00000000000..8bdae2e456a
--- /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<ImportVMTaskVO, Long> implements ImportVMTaskDao {
+
+ private SearchBuilder<ImportVMTaskVO> 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<ImportVMTaskVO> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted) {
+ SearchCriteria<ImportVMTaskVO> 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 showCompleted ? listIncludingRemovedBy(sc) : 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 7e9d6c3b54f..0656d5e3c44 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 @@
<bean id="gpuCardDaoImpl" class="com.cloud.gpu.dao.GpuCardDaoImpl" />
<bean id="gpuDeviceDaoImpl" class="com.cloud.gpu.dao.GpuDeviceDaoImpl" />
<bean id="vgpuProfileDaoImpl" class="com.cloud.gpu.dao.VgpuProfileDaoImpl" />
+ <bean id="importVMTaskDaoImpl" class="com.cloud.vm.dao.ImportVMTaskDaoImpl" />
</beans>
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 5b9b4272263..a14a9fef8ad 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
@@ -48,5 +48,35 @@ CALL `cloud`.`IDEMPOTENT_ADD_COLUMN`('cloud.backup_repository', 'cross_zone_inst
UPDATE `cloud`.`storage_pool_details` SET display = 0 WHERE name LIKE '%password%';
UPDATE `cloud`.`storage_pool_details` SET display = 0 WHERE name LIKE '%token%';
+-- 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',
+ `vm_id` bigint unsigned COMMENT 'VM 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',
+ `duration` bigint unsigned COMMENT 'Duration in milliseconds for the completed tasks',
+ `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__vm_id` FOREIGN KEY `fk_import_vm_task__vm_id` (`vm_id`) REFERENCES `vm_instance`(`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;
+
CALL `cloud`.`INSERT_EXTENSION_IF_NOT_EXISTS`('MaaS', 'Baremetal Extension for Canonical MaaS written in Python', 'MaaS/maas.py');
CALL `cloud`.`INSERT_EXTENSION_DETAIL_IF_NOT_EXISTS`('MaaS', 'orchestratorrequirespreparevm', 'true', 0);
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
index a86efeb8a1f..0aa094e56d9 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java
@@ -882,6 +882,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
protected StorageSubsystemCommandHandler storageHandler;
private boolean convertInstanceVerboseMode = false;
+ private String[] convertInstanceEnv = null;
protected boolean dpdkSupport = false;
protected String dpdkOvsPath;
protected String directDownloadTemporaryDownloadPath;
@@ -946,6 +947,10 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
return convertInstanceVerboseMode;
}
+ public String[] getConvertInstanceEnv() {
+ return convertInstanceEnv;
+ }
+
/**
* Defines resource's public and private network interface according to what is configured in agent.properties.
*/
@@ -1146,6 +1151,11 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
convertInstanceVerboseMode = BooleanUtils.isTrue(AgentPropertiesFileHandler.getPropertyValue(AgentProperties.VIRTV2V_VERBOSE_ENABLED));
+ String convertEnvTmpDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.CONVERT_ENV_TMPDIR);
+ String convertEnvVirtv2vTmpDir = AgentPropertiesFileHandler.getPropertyValue(AgentProperties.CONVERT_ENV_VIRTV2V_TMPDIR);
+
+ setConvertInstanceEnv(convertEnvTmpDir, convertEnvVirtv2vTmpDir);
+
pool = (String)params.get("pool");
if (pool == null) {
pool = "/root";
@@ -1422,6 +1432,22 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
return true;
}
+ private void setConvertInstanceEnv(String convertEnvTmpDir, String convertEnvVirtv2vTmpDir) {
+ if (StringUtils.isAllBlank(convertEnvTmpDir, convertEnvVirtv2vTmpDir)) {
+ return;
+ }
+ if (StringUtils.isNotBlank(convertEnvTmpDir) && StringUtils.isNotBlank(convertEnvVirtv2vTmpDir)) {
+ convertInstanceEnv = new String[2];
+ convertInstanceEnv[0] = String.format("%s=%s", "TMPDIR", convertEnvTmpDir);
+ convertInstanceEnv[1] = String.format("%s=%s", "VIRT_V2V_TMPDIR", convertEnvVirtv2vTmpDir);
+ } else {
+ convertInstanceEnv = new String[1];
+ String key = StringUtils.isNotBlank(convertEnvTmpDir) ? "TMPDIR" : "VIRT_V2V_TMPDIR";
+ String value = StringUtils.isNotBlank(convertEnvTmpDir) ? convertEnvTmpDir : convertEnvVirtv2vTmpDir;
+ convertInstanceEnv[0] = String.format("%s=%s", key, value);
+ }
+ }
+
/**
* Parses a string containing whitespace-separated CPU feature names and converts it into a list.
*
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 abc408f20f6..93895349a6e 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
@@ -20,6 +20,7 @@ package com.cloud.hypervisor.kvm.resource.wrapper;
import java.net.URLEncoder;
import java.nio.charset.Charset;
+import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@@ -57,11 +58,13 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
Hypervisor.HypervisorType destinationHypervisorType = cmd.getDestinationHypervisorType();
DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation();
long timeout = (long) cmd.getWait() * 1000;
+ String extraParams = cmd.getExtraParams();
+ String originalVMName = cmd.getOriginalVMName(); // For logging purposes, as the sourceInstance may have been cloned
if (cmd.getCheckConversionSupport() && !serverResource.hostSupportsInstanceConversion()) {
String msg = String.format("Cannot convert the instance %s from VMware as the virt-v2v binary is not found. " +
"Please install virt-v2v%s on the host before attempting the instance conversion.", sourceInstanceName, serverResource.isUbuntuOrDebianHost()? ", nbdkit" : "");
- logger.info(msg);
+ logger.info(String.format("(%s) %s", originalVMName, msg));
return new Answer(cmd, false, msg);
}
@@ -69,25 +72,25 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
String err = destinationHypervisorType != Hypervisor.HypervisorType.KVM ?
String.format("The destination hypervisor type is %s, KVM was expected, cannot handle it", destinationHypervisorType) :
String.format("The source hypervisor type %s is not supported for KVM conversion", sourceHypervisorType);
- logger.error(err);
+ logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err);
}
final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr);
- logger.info(String.format("Attempting to convert the instance %s from %s to KVM",
- sourceInstanceName, sourceHypervisorType));
+ logger.info(String.format("(%s) Attempting to convert the instance %s from %s to KVM",
+ originalVMName, sourceInstanceName, sourceHypervisorType));
final String temporaryConvertPath = temporaryStoragePool.getLocalPath();
String ovfTemplateDirOnConversionLocation;
String sourceOVFDirPath;
boolean ovfExported = false;
if (cmd.getExportOvfToConversionLocation()) {
- String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance);
+ String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance, originalVMName);
if (StringUtils.isBlank(exportInstanceOVAUrl)) {
String err = String.format("Couldn't export OVA for the VM %s, due to empty url", sourceInstanceName);
- logger.error(err);
+ logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err);
}
@@ -98,10 +101,10 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
ovfTemplateDirOnConversionLocation = UUID.randomUUID().toString();
temporaryStoragePool.createFolder(ovfTemplateDirOnConversionLocation);
sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
- ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, timeout);
+ ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, originalVMName, timeout);
if (!ovfExported) {
String err = String.format("Export OVA for the VM %s failed", sourceInstanceName);
- logger.error(err);
+ logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err);
}
sourceOVFDirPath = String.format("%s%s/", sourceOVFDirPath, sourceInstanceName);
@@ -110,38 +113,40 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
}
- logger.info(String.format("Attempting to convert the OVF %s of the instance %s from %s to KVM", ovfTemplateDirOnConversionLocation, sourceInstanceName, sourceHypervisorType));
+ logger.info(String.format("(%s) Attempting to convert the OVF %s of the instance %s from %s to KVM",
+ originalVMName, ovfTemplateDirOnConversionLocation, sourceInstanceName, sourceHypervisorType));
+
final String temporaryConvertUuid = UUID.randomUUID().toString();
boolean verboseModeEnabled = serverResource.isConvertInstanceVerboseModeEnabled();
boolean cleanupSecondaryStorage = false;
try {
- boolean result = performInstanceConversion(sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid,
- timeout, verboseModeEnabled);
+ boolean result = performInstanceConversion(originalVMName, sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid,
+ timeout, verboseModeEnabled, extraParams, serverResource);
if (!result) {
String err = String.format(
"The virt-v2v conversion for the OVF %s failed. Please check the agent logs " +
"for the virt-v2v output. Please try on a different kvm host which " +
"has a different virt-v2v version.",
ovfTemplateDirOnConversionLocation);
- logger.error(err);
+ logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err);
}
return new ConvertInstanceAnswer(cmd, temporaryConvertUuid);
} catch (Exception e) {
String error = String.format("Error converting instance %s from %s, due to: %s",
sourceInstanceName, sourceHypervisorType, e.getMessage());
- logger.error(error, e);
+ logger.error(String.format("(%s) %s", originalVMName, error), e);
cleanupSecondaryStorage = true;
return new Answer(cmd, false, error);
} finally {
if (ovfExported && StringUtils.isNotBlank(ovfTemplateDirOnConversionLocation)) {
String sourceOVFDir = String.format("%s/%s", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
- logger.debug("Cleaning up exported OVA at dir " + sourceOVFDir);
+ logger.debug("({}) Cleaning up exported OVA at dir: {}", originalVMName, sourceOVFDir);
FileUtil.deletePath(sourceOVFDir);
}
if (cleanupSecondaryStorage && conversionTemporaryLocation instanceof NfsTO) {
- logger.debug("Cleaning up secondary storage temporary location");
+ logger.debug("({}) Cleaning up secondary storage temporary location", originalVMName);
storagePoolMgr.deleteStoragePool(temporaryStoragePool.getType(), temporaryStoragePool.getUuid());
}
}
@@ -163,15 +168,15 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
supportedInstanceConvertSourceHypervisors.contains(sourceHypervisorType);
}
- private String getExportInstanceOVAUrl(RemoteInstanceTO sourceInstance) {
+ private String getExportInstanceOVAUrl(RemoteInstanceTO sourceInstance, String originalVMName) {
String url = null;
if (sourceInstance.getHypervisorType() == Hypervisor.HypervisorType.VMware) {
- url = getExportOVAUrlFromRemoteInstance(sourceInstance);
+ url = getExportOVAUrlFromRemoteInstance(sourceInstance, originalVMName);
}
return url;
}
- private String getExportOVAUrlFromRemoteInstance(RemoteInstanceTO vmwareInstance) {
+ private String getExportOVAUrlFromRemoteInstance(RemoteInstanceTO vmwareInstance, String originalVMName) {
String vcenter = vmwareInstance.getVcenterHost();
String username = vmwareInstance.getVcenterUsername();
String password = vmwareInstance.getVcenterPassword();
@@ -182,7 +187,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
String encodedUsername = encodeUsername(username);
String encodedPassword = encodeUsername(password);
if (StringUtils.isNotBlank(path)) {
- logger.info("VM path: {}", path);
+ logger.info("({}) VM path: {}", originalVMName, path);
return String.format("vi://%s:%s@%s/%s/%s/%s",
encodedUsername, encodedPassword, vcenter, datacenter, path, vm);
}
@@ -201,7 +206,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
private boolean exportOVAFromVMOnVcenter(String vmExportUrl,
String targetOvfDir,
int noOfThreads,
- long timeout) {
+ String originalVMName, long timeout) {
Script script = new Script("ovftool", timeout, logger);
script.add("--noSSLVerify");
if (noOfThreads > 1) {
@@ -210,17 +215,18 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
script.add(vmExportUrl);
script.add(targetOvfDir);
- String logPrefix = "export ovf";
+ String logPrefix = String.format("(%s) export ovf", originalVMName);
OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix);
script.execute(outputLogger);
int exitValue = script.getExitValue();
return exitValue == 0;
}
- protected boolean performInstanceConversion(String sourceOVFDirPath,
+ protected boolean performInstanceConversion(String originalVMName, String sourceOVFDirPath,
String temporaryConvertFolder,
String temporaryConvertUuid,
- long timeout, boolean verboseModeEnabled) {
+ long timeout, boolean verboseModeEnabled, String extraParams,
+ LibvirtComputingResource serverResource) {
Script script = new Script("virt-v2v", timeout, logger);
script.add("--root", "first");
script.add("-i", "ova");
@@ -232,14 +238,33 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
if (verboseModeEnabled) {
script.add("-v");
}
+ if (StringUtils.isNotBlank(extraParams)) {
+ addExtraParamsToScript(extraParams, script);
+ }
- String logPrefix = String.format("virt-v2v ovf source: %s progress", sourceOVFDirPath);
+ String logPrefix = String.format("(%s) virt-v2v ovf source: %s progress", originalVMName, sourceOVFDirPath);
OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix);
script.execute(outputLogger);
int exitValue = script.getExitValue();
return exitValue == 0;
}
+ protected void addExtraParamsToScript(String extraParams, Script script) {
+ List<String> separatedArgs = Arrays.asList(extraParams.split(" "));
+ int i = 0;
+ while (i < separatedArgs.size()) {
+ String current = separatedArgs.get(i);
+ String next = (i + 1) < separatedArgs.size() ? separatedArgs.get(i + 1) : null;
+ if (next == null || next.startsWith("-")) {
+ script.add(current);
+ i = i + 1;
+ } else {
+ script.add(current, next);
+ i = i + 2;
+ }
+ }
+ }
+
protected String encodeUsername(String username) {
return URLEncoder.encode(username, Charset.defaultCharset());
}
diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java
index 0eec15cf733..5602da15679 100644
--- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java
+++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtImportConvertedInstanceCommandWrapper.java
@@ -67,6 +67,7 @@ public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper
List<String> destinationStoragePools = cmd.getDestinationStoragePools();
DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation();
final String temporaryConvertUuid = cmd.getTemporaryConvertUuid();
+ final boolean forceConvertToPool = cmd.isForceConvertToPool();
final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr);
@@ -80,13 +81,18 @@ public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper
getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid) :
getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath);
- List<KVMPhysicalDisk> destinationDisks = moveTemporaryDisksToDestination(temporaryDisks,
- destinationStoragePools, storagePoolMgr);
-
- cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid);
+ List<KVMPhysicalDisk> disks = null;
+ if (forceConvertToPool) {
+ // Force flag to use the conversion path, no need to move disks
+ disks = temporaryDisks;
+ } else {
+ disks = moveTemporaryDisksToDestination(temporaryDisks,
+ destinationStoragePools, storagePoolMgr);
+ cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid);
+ }
UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid,
- destinationDisks, xmlParser);
+ disks, xmlParser);
return new ImportConvertedInstanceAnswer(cmd, convertedInstanceTO);
} catch (Exception e) {
String error = String.format("Error converting instance %s from %s, due to: %s",
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 b369cf25f3d..4d55ac2bc73 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
@@ -128,6 +128,7 @@ public class LibvirtConvertInstanceCommandWrapperTest {
Mockito.when(cmd.getWait()).thenReturn(14400);
Mockito.when(cmd.getConversionTemporaryLocation()).thenReturn(secondaryDataStore);
Mockito.when(cmd.getCheckConversionSupport()).thenReturn(checkConversionSupport);
+ Mockito.when(cmd.getOriginalVMName()).thenReturn(vmName);
return cmd;
}
@@ -166,8 +167,26 @@ public class LibvirtConvertInstanceCommandWrapperTest {
Answer answer = convertInstanceCommandWrapper.execute(cmd, libvirtComputingResourceMock);
Assert.assertFalse(answer.getResult());
- Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(),
- Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean());
+ Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(), Mockito.anyString(),
+ Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean(), Mockito.nullable(String.class), Mockito.any(LibvirtComputingResource.class));
}
}
+
+ @Test
+ public void testAddExtraParamsToScriptSameKeysAndValues() {
+ Script script = Mockito.mock(Script.class);
+ String extraParams = "--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254";
+ convertInstanceCommandWrapper.addExtraParamsToScript(extraParams, script);
+ Mockito.verify(script).add("--mac", "00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254");
+ }
+
+ @Test
+ public void testAddExtraParamsToScriptDifferentArgs() {
+ Script script = Mockito.mock(Script.class);
+ String extraParams = "--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254 -x -v";
+ convertInstanceCommandWrapper.addExtraParamsToScript(extraParams, script);
+ Mockito.verify(script).add("--mac", "00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254");
+ Mockito.verify(script).add("-x");
+ Mockito.verify(script).add("-v");
+ }
}
diff --git a/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java
new file mode 100644
index 00000000000..fa36627e864
--- /dev/null
+++ b/server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java
@@ -0,0 +1,225 @@
+// 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.vm;
+
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.DataCenterVO;
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.host.Host;
+import com.cloud.host.HostVO;
+import com.cloud.host.dao.HostDao;
+import com.cloud.user.Account;
+import com.cloud.user.AccountService;
+import com.cloud.utils.DateUtil;
+import com.cloud.vm.ImportVMTaskVO;
+import com.cloud.vm.UserVmVO;
+import com.cloud.vm.dao.ImportVMTaskDao;
+import com.cloud.vm.dao.UserVmDao;
+import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd;
+import org.apache.cloudstack.api.response.ImportVMTaskResponse;
+import org.apache.cloudstack.api.response.ListResponse;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import javax.inject.Inject;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+import static org.apache.cloudstack.vm.ImportVmTask.Step.CloningInstance;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.Completed;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.ConvertingInstance;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.Importing;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.Prepare;
+
+public class ImportVmTasksManagerImpl implements ImportVmTasksManager {
+
+ protected Logger logger = LogManager.getLogger(ImportVmTasksManagerImpl.class);
+
+ @Inject
+ private ImportVMTaskDao importVMTaskDao;
+ @Inject
+ private DataCenterDao dataCenterDao;
+ @Inject
+ private AccountService accountService;
+ @Inject
+ private HostDao hostDao;
+ @Inject
+ private UserVmDao userVmDao;
+
+ public ImportVmTasksManagerImpl() {
+ }
+
+ @Override
+ public ListResponse<ImportVMTaskResponse> listImportVMTasks(ListImportVMTasksCmd cmd) {
+ Long zoneId = cmd.getZoneId();
+ Long accountId = cmd.getAccountId();
+ String vcenter = cmd.getVcenter();
+ Long convertHostId = cmd.getConvertHostId();
+ boolean listAll = cmd.isListAll();
+ boolean showCompleted = cmd.isShowCompleted();
+
+ List<ImportVMTaskVO> tasks;
+ if (listAll) {
+ tasks = importVMTaskDao.listAll();
+ } else {
+ tasks = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId, showCompleted);
+ }
+
+ List<ImportVMTaskResponse> responses = new ArrayList<>();
+ for (ImportVMTaskVO task : tasks) {
+ responses.add(createImportVMTaskResponse(task));
+ }
+ ListResponse<ImportVMTaskResponse> listResponses = new ListResponse<>();
+ listResponses.setResponses(responses, responses.size());
+ return listResponses;
+ }
+
+ @Override
+ public ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName, String vcenter, String datacenterName, String sourceVMName, Host convertHost, Host 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, Host convertHost, Host importHost,
+ ImportVMTaskVO.Step step, Date updatedDate) {
+ String sourceVMName = importVMTaskVO.getSourceVMName();
+ String vcenter = importVMTaskVO.getVcenter();
+ String datacenter = importVMTaskVO.getDatacenter();
+
+ StringBuilder stringBuilder = new StringBuilder();
+ if (Completed == step) {
+ stringBuilder.append("Completed at ").append(DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate));
+ } else {
+ 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()));
+ } else if (Prepare == step) {
+ stringBuilder.append("Preparing to convert Vmware instance");
+ }
+ }
+ return stringBuilder.toString();
+ }
+
+ @Override
+ public void updateImportVMTaskStep(ImportVmTask importVMTask, DataCenter zone, Account owner, Host convertHost,
+ Host importHost, Long vmId, ImportVmTask.Step step) {
+ ImportVMTaskVO importVMTaskVO = (ImportVMTaskVO) importVMTask;
+ 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);
+ if (Completed == step) {
+ Duration duration = Duration.between(importVMTaskVO.getCreated().toInstant(), updatedDate.toInstant());
+ importVMTaskVO.setDuration(duration.toMillis());
+ importVMTaskVO.setVmId(vmId);
+ }
+ importVMTaskDao.update(importVMTaskVO.getId(), importVMTaskVO);
+ }
+
+ @Override
+ public boolean removeImportVMTask(long taskId) {
+ return importVMTaskDao.remove(taskId);
+ }
+
+ 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.setDisplayName(task.getDisplayName());
+ response.setStep(getStepDisplayField(task.getStep()));
+ response.setDescription(task.getDescription());
+
+ Date updated = task.getUpdated();
+ Date currentDate = new Date();
+ if (updated != null && Completed != task.getStep()) {
+ Duration stepDuration = Duration.between(updated.toInstant(), currentDate.toInstant());
+ response.setStepDuration(getDurationDisplay(stepDuration.toMillis()));
+ }
+ if (Completed == task.getStep()) {
+ response.setStepDuration(getDurationDisplay(task.getDuration()));
+ } else {
+ Duration totalDuration = Duration.between(task.getCreated().toInstant(), currentDate.toInstant());
+ response.setDuration(getDurationDisplay(totalDuration.toMillis()));
+ }
+ HostVO host = hostDao.findById(task.getConvertHostId());
+ if (host != null) {
+ response.setConvertInstanceHostId(host.getUuid());
+ response.setConvertInstanceHostName(host.getName());
+ }
+ if (task.getVmId() != null) {
+ UserVmVO userVm = userVmDao.findById(task.getVmId());
+ response.setVirtualMachineId(userVm.getUuid());
+ }
+ response.setCreated(task.getCreated());
+ response.setLastUpdated(task.getUpdated());
+ response.setObjectName("importvmtask");
+ return response;
+ }
+
+ protected String getStepDisplayField(ImportVMTaskVO.Step step) {
+ int totalSteps = ImportVMTaskVO.Step.values().length;
+ return String.format("[%s/%s] %s", step.ordinal() + 1, totalSteps, step.name());
+ }
+
+ protected static String getDurationDisplay(Long durationMs) {
+ if (durationMs == null) {
+ return null;
+ }
+ long hours = durationMs / (1000 * 60 * 60);
+ long minutes = (durationMs / (1000 * 60)) % 60;
+ long seconds = (durationMs / 1000) % 60;
+
+ StringBuilder result = new StringBuilder();
+ if (hours > 0) {
+ result.append(String.format("%s hs ", hours));
+ }
+ if (minutes > 0) {
+ result.append(String.format("%s min ", minutes));
+ }
+ if (seconds > 0) {
+ result.append(String.format("%s secs", seconds));
+ }
+ return result.toString();
+ }
+}
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 187b315dd35..51623b56ec8 100644
--- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
+++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java
@@ -122,6 +122,7 @@ import com.cloud.user.dao.UserDao;
import com.cloud.uservm.UserVm;
import com.cloud.utils.LogUtils;
import com.cloud.utils.Pair;
+import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils;
@@ -150,6 +151,7 @@ import org.apache.cloudstack.api.ResponseObject;
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;
@@ -173,6 +175,8 @@ import org.apache.cloudstack.storage.volume.VolumeOnStorageTO;
import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
+import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -180,7 +184,9 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -190,6 +196,10 @@ import java.util.stream.Collectors;
import static org.apache.cloudstack.api.ApiConstants.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_IOPS;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.CloningInstance;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.Completed;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.ConvertingInstance;
+import static org.apache.cloudstack.vm.ImportVmTask.Step.Importing;
public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
public static final String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso";
@@ -198,6 +208,28 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
private static final List<Hypervisor.HypervisorType> importUnmanagedInstancesSupportedHypervisors =
Arrays.asList(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.KVM);
+ private static final List<Storage.StoragePoolType> forceConvertToPoolAllowedTypes =
+ Arrays.asList(Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.Filesystem,
+ Storage.StoragePoolType.SharedMountPoint);
+
+ ConfigKey<Boolean> ConvertVmwareInstanceToKvmExtraParamsAllowed = new ConfigKey<>(Boolean.class,
+ "convert.vmware.instance.to.kvm.extra.params.allowed",
+ "Advanced",
+ "false",
+ "Disabled by default. If enabled, allows extra parameters to be passed to the virt-v2v binary on KVM conversion hosts",
+ true,
+ ConfigKey.Scope.Global,
+ null);
+
+ ConfigKey<String> ConvertVmwareInstanceToKvmExtraParamsAllowedList = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
+ String.class,
+ "convert.vmware.instance.to.kvm.extra.params.allowed.list",
+ "",
+ "Comma separated list of allowed extra parameters to be passed to the virt-v2v binary on KVM conversion hosts",
+ true,
+ ConfigKey.Kind.CSV,
+ null);
+
@Inject
private AgentManager agentManager;
@Inject
@@ -282,6 +314,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
private ImageStoreDao imageStoreDao;
@Inject
private DataStoreManager dataStoreManager;
+ @Inject
+ private ImportVmTasksManager importVmTasksManager;
protected Gson gson;
@@ -557,11 +591,12 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
}
if (storagePool == null) {
- List<StoragePoolVO> pools = primaryDataStoreDao.listPoolsByCluster(cluster.getId());
+ Set<StoragePoolVO> pools = new HashSet<>(primaryDataStoreDao.listPoolsByCluster(cluster.getId()));
pools.addAll(primaryDataStoreDao.listByDataCenterId(zone.getId()));
+ boolean isNameUuid = StringUtils.isNotBlank(dsName) && UuidUtils.isUuid(dsName);
for (StoragePool pool : pools) {
String searchPoolParam = StringUtils.isNotBlank(dsPath) ? dsPath : dsName;
- if (StringUtils.contains(pool.getPath(), searchPoolParam) &&
+ if ((StringUtils.contains(pool.getPath(), searchPoolParam) || isNameUuid && pool.getUuid().equals(dsName)) &&
volumeApiService.doesStoragePoolSupportDiskOffering(pool, diskOffering)) {
storagePool = pool;
break;
@@ -1418,6 +1453,33 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
return responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVm).get(0);
}
+ protected void checkExtraParamsAllowed(String extraParams) {
+ if (StringUtils.isBlank(extraParams)) {
+ return;
+ }
+ if (BooleanUtils.isFalse(ConvertVmwareInstanceToKvmExtraParamsAllowed.value())) {
+ throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
+ "Extra parameters for Vmware to KVM conversion are disabled by the administrator");
+ }
+ String allowedParamsStr = ConvertVmwareInstanceToKvmExtraParamsAllowedList.value();
+ if (StringUtils.isBlank(allowedParamsStr)) {
+ throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
+ "Extra parameters for Vmware to KVM conversion are enabled but the allowed list of parameters is empty");
+ }
+ List<String> allowedParams = Arrays.asList(allowedParamsStr.split(","));
+ List<String> sanitizedParams = Arrays.asList(extraParams.split(" "))
+ .stream()
+ .filter(x -> x.startsWith("-"))
+ .map(s -> s.replaceFirst("^-+", "").trim()) //Remove the starting hyphens as in --X or -x
+ .collect(Collectors.toList());
+ for (String param : sanitizedParams) {
+ if (!allowedParams.contains(param)) {
+ throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
+ String.format("The parameter %s is not allowed by the administrator", param));
+ }
+ }
+ }
+
private long getUserIdForImportInstance(Account owner) {
long userId = CallContext.current().getCallingUserId();
List<UserVO> userVOs = userDao.listByAccount(owner.getAccountId());
@@ -1513,6 +1575,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
String source = cmd.getImportSource().toUpperCase();
ImportSource importSource = Enum.valueOf(ImportSource.class, source);
if (ImportSource.VMWARE == importSource || ImportSource.UNMANAGED == importSource) {
+ checkExtraParamsAllowed(cmd.getExtraParams());
return baseImportInstance(cmd);
} else {
return importKvmInstance(cmd);
@@ -1621,6 +1684,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
Long convertInstanceHostId = cmd.getConvertInstanceHostId();
Long importInstanceHostId = cmd.getImportInstanceHostId();
Long convertStoragePoolId = cmd.getConvertStoragePoolId();
+ String extraParams = cmd.getExtraParams();
+ boolean forceConvertToPool = cmd.getForceConvertToPool();
if ((existingVcenterId == null && vcenter == null) || (existingVcenterId != null && vcenter != null)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
@@ -1631,6 +1696,10 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
"Please set all the information for a vCenter IP/Name, datacenter, username and password");
}
+ checkConversionStoragePool(convertStoragePoolId, forceConvertToPool);
+
+ checkExtraParamsAllowed(extraParams);
+
if (existingVcenterId != null) {
VmwareDatacenterVO existingDC = vmwareDatacenterDao.findById(existingVcenterId);
if (existingDC == null) {
@@ -1648,6 +1717,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
UnmanagedInstanceTO sourceVMwareInstance = null;
DataStoreTO temporaryConvertLocation = null;
String ovfTemplateOnConvertLocation = null;
+ ImportVmTask importVMTask = null;
try {
HostVO convertHost = selectKVMHostForConversionInCluster(destinationCluster, convertInstanceHostId);
HostVO importHost = selectKVMHostForImportingInCluster(destinationCluster, importInstanceHostId);
@@ -1656,12 +1726,21 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
"instance {} from VMware to KVM ", convertHost, sourceVMName);
temporaryConvertLocation = selectInstanceConversionTemporaryLocation(
- destinationCluster, convertHost, convertStoragePoolId);
- List<StoragePoolVO> convertStoragePools = findInstanceConversionStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap);
+ destinationCluster, convertHost, importHost, convertStoragePoolId, forceConvertToPool);
+ List<StoragePoolVO> convertStoragePools = findInstanceConversionDestinationStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap, temporaryConvertLocation, forceConvertToPool);
+
long importStartTime = System.currentTimeMillis();
+ importVMTask = importVmTasksManager.createImportVMTaskRecord(zone, owner, userId, displayName, vcenter, datacenterName, sourceVMName,
+ convertHost, importHost);
+ importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, CloningInstance);
+
+ // sourceVMwareInstance could be a cloned instance from sourceVMName, of the sourceVMName itself if its powered off.
+ // isClonedInstance indicates if the VM is a clone of sourceVMName
+
Pair<UnmanagedInstanceTO, Boolean> sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName);
sourceVMwareInstance = sourceInstanceDetails.first();
isClonedInstance = sourceInstanceDetails.second();
+
boolean isWindowsVm = sourceVMwareInstance.getOperatingSystem().toLowerCase().contains("windows");
if (isWindowsVm) {
checkConversionSupportOnHost(convertHost, sourceVMName, true);
@@ -1672,22 +1751,25 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
if (cmd.getForceMsToImportVmFiles() || !conversionSupportAnswer.isOvfExportSupported()) {
// Uses MS for OVF export to temporary conversion location
int noOfThreads = UnmanagedVMsManager.ThreadsOnMSToImportVMwareVMFiles.value();
+ importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, ConvertingInstance);
ovfTemplateOnConvertLocation = createOvfTemplateOfSourceVmwareUnmanagedInstance(
vcenter, datacenterName, username, password, clusterName, sourceHostName,
sourceVMwareInstance.getName(), temporaryConvertLocation, noOfThreads);
convertedInstance = convertVmwareInstanceToKVMWithOVFOnConvertLocation(sourceVMName,
sourceVMwareInstance, convertHost, importHost, convertStoragePools,
serviceOffering, dataDiskOfferingMap, temporaryConvertLocation,
- ovfTemplateOnConvertLocation);
+ ovfTemplateOnConvertLocation, forceConvertToPool, extraParams);
} else {
// Uses KVM Host for OVF export to temporary conversion location, through ovftool
+ importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, ConvertingInstance);
convertedInstance = convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation(
sourceVMName, sourceVMwareInstance, convertHost, importHost,
convertStoragePools, serviceOffering, dataDiskOfferingMap,
- temporaryConvertLocation, vcenter, username, password, datacenterName);
+ temporaryConvertLocation, vcenter, username, password, datacenterName, forceConvertToPool, extraParams);
}
sanitizeConvertedInstance(convertedInstance, sourceVMwareInstance);
+ importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, Importing);
UserVm userVm = importVirtualMachineInternal(convertedInstance, null, zone, destinationCluster, null,
template, displayName, hostName, caller, owner, userId,
serviceOffering, dataDiskOfferingMap,
@@ -1696,6 +1778,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
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, displayName, displayName, timeElapsedInSecs, (ovfTemplateOnConvertLocation != null)? "MS" : "KVM Host", sourceVMwareInstance.getOperatingSystem(), sourceVMwareInstance.getPowerState(), sourceVMwareInstance.getDisks(), sourceVMwareInstance.getNics()));
+ importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, userVm.getId(), Completed);
+ importVmTasksManager.removeImportVMTask(importVMTask.getId());
return userVm;
} catch (CloudRuntimeException e) {
logger.error(String.format("Error importing VM: %s", e.getMessage()), e);
@@ -1712,6 +1796,30 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
}
}
+ /**
+ * Check whether the conversion storage pool exists and is suitable for the conversion or not.
+ * Secondary storage is only allowed when forceConvertToPool is false.
+ * @param convertStoragePoolId the ID of the storage pool (primary or secondary)
+ * @param forceConvertToPool when true, only primary storage pool must be allowed
+ * @throws CloudRuntimeException in case these requirements are not met
+ */
+ protected void checkConversionStoragePool(Long convertStoragePoolId, boolean forceConvertToPool) {
+ if (forceConvertToPool && convertStoragePoolId == null) {
+ String msg = "The parameter forceconverttopool is set to true, but a primary storage pool has not been provided for conversion";
+ logFailureAndThrowException(msg);
+ }
+ if (convertStoragePoolId != null) {
+ StoragePoolVO selectedStoragePool = primaryDataStoreDao.findById(convertStoragePoolId);
+ if (selectedStoragePool == null) {
+ logFailureAndThrowException(String.format("Cannot find a storage pool with ID %s", convertStoragePoolId));
+ }
+ if (forceConvertToPool && !forceConvertToPoolAllowedTypes.contains(selectedStoragePool.getPoolType())) {
+ logFailureAndThrowException(String.format("The selected storage pool %s does not support direct conversion " +
+ "as its type %s", selectedStoragePool.getName(), selectedStoragePool.getPoolType().name()));
+ }
+ }
+ }
+
private void checkNetworkingBeforeConvertingVmwareInstance(DataCenter zone, Account owner, String displayName,
String hostName, UnmanagedInstanceTO sourceVMwareInstance,
Map<String, Long> nicNetworkMap,
@@ -1953,20 +2061,25 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
String sourceVM, UnmanagedInstanceTO sourceVMwareInstance, HostVO convertHost,
HostVO importHost, List<StoragePoolVO> convertStoragePools,
ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap,
- DataStoreTO temporaryConvertLocation, String ovfTemplateDirConvertLocation
- ) {
- logger.debug("Delegating the conversion of instance {} from VMware to KVM to the host {} using OVF {} on conversion datastore",
- sourceVM, convertHost, ovfTemplateDirConvertLocation);
+ DataStoreTO temporaryConvertLocation, String ovfTemplateDirConvertLocation,
+ boolean forceConvertToPool, String extraParams) {
+
+ logger.debug("Delegating the conversion of instance {} from VMware to KVM to the host {} using OVF {} on conversion datastore",
+ sourceVM, convertHost, ovfTemplateDirConvertLocation);
RemoteInstanceTO remoteInstanceTO = new RemoteInstanceTO(sourceVM);
List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap);
ConvertInstanceCommand cmd = new ConvertInstanceCommand(remoteInstanceTO,
- Hypervisor.HypervisorType.KVM, temporaryConvertLocation, ovfTemplateDirConvertLocation, false, false);
+ Hypervisor.HypervisorType.KVM, temporaryConvertLocation,
+ ovfTemplateDirConvertLocation, false, false, sourceVM);
+ if (StringUtils.isNotBlank(extraParams)) {
+ cmd.setExtraParams(extraParams);
+ }
int timeoutSeconds = UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.value() * 60 * 60;
cmd.setWait(timeoutSeconds);
return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM,
- remoteInstanceTO, destinationStoragePools, temporaryConvertLocation);
+ remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, forceConvertToPool);
}
private UnmanagedInstanceTO convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation(
@@ -1974,14 +2087,13 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
HostVO importHost, List<StoragePoolVO> convertStoragePools,
ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap,
DataStoreTO temporaryConvertLocation, String vcenterHost, String vcenterUsername,
- String vcenterPassword, String datacenterName
- ) {
+ String vcenterPassword, String datacenterName, boolean forceConvertToPool, String extraParams) {
logger.debug("Delegating the conversion of instance {} from VMware to KVM to the host {} after OVF export through ovftool", sourceVM, convertHost);
RemoteInstanceTO remoteInstanceTO = new RemoteInstanceTO(sourceVMwareInstance.getName(), sourceVMwareInstance.getPath(), vcenterHost, vcenterUsername, vcenterPassword, datacenterName);
List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap);
ConvertInstanceCommand cmd = new ConvertInstanceCommand(remoteInstanceTO,
- Hypervisor.HypervisorType.KVM, temporaryConvertLocation, null, false, true);
+ Hypervisor.HypervisorType.KVM, temporaryConvertLocation, null, false, true, sourceVM);
int timeoutSeconds = UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.value() * 60 * 60;
cmd.setWait(timeoutSeconds);
int noOfThreads = UnmanagedVMsManager.ThreadsOnKVMHostToImportVMwareVMFiles.value();
@@ -1990,16 +2102,19 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
noOfThreads = sourceVMwareInstance.getDisks().size();
}
cmd.setThreadsCountToExportOvf(noOfThreads);
-
+ if (StringUtils.isNotBlank(extraParams)) {
+ cmd.setExtraParams(extraParams);
+ }
return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM,
- remoteInstanceTO, destinationStoragePools, temporaryConvertLocation);
+ remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, forceConvertToPool);
}
private UnmanagedInstanceTO convertAndImportToKVM(ConvertInstanceCommand convertInstanceCommand, HostVO convertHost, HostVO importHost,
String sourceVM,
RemoteInstanceTO remoteInstanceTO,
List<String> destinationStoragePools,
- DataStoreTO temporaryConvertLocation) {
+ DataStoreTO temporaryConvertLocation,
+ boolean forceConvertToPool) {
Answer convertAnswer;
try {
convertAnswer = agentManager.send(convertHost.getId(), convertInstanceCommand);
@@ -2021,7 +2136,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
try {
ImportConvertedInstanceCommand importCmd = new ImportConvertedInstanceCommand(
remoteInstanceTO, destinationStoragePools, temporaryConvertLocation,
- ((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid());
+ ((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid(), forceConvertToPool);
importAnswer = agentManager.send(importHost.getId(), importCmd);
} catch (AgentUnavailableException | OperationTimedoutException e) {
String err = String.format(
@@ -2042,17 +2157,23 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
return ((ImportConvertedInstanceAnswer) importAnswer).getConvertedInstance();
}
- private List<StoragePoolVO> findInstanceConversionStoragePoolsInCluster(
+ private List<StoragePoolVO> findInstanceConversionDestinationStoragePoolsInCluster(
Cluster destinationCluster, ServiceOfferingVO serviceOffering,
- Map<String, Long> dataDiskOfferingMap
- ) {
- List<StoragePoolVO> pools = new ArrayList<>();
- pools.addAll(primaryDataStoreDao.findClusterWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
- pools.addAll(primaryDataStoreDao.findZoneWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getDataCenterId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
- if (pools.isEmpty()) {
- String msg = String.format("Cannot find suitable storage pools in the cluster %s for the conversion", destinationCluster.getName());
- logger.error(msg);
- throw new CloudRuntimeException(msg);
+ Map<String, Long> dataDiskOfferingMap,
+ DataStoreTO temporaryConvertLocation, boolean forceConvertToPool) {
+ List<StoragePoolVO> poolsList;
+ if (!forceConvertToPool) {
+ Set<StoragePoolVO> pools = new HashSet<>(primaryDataStoreDao.findClusterWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
+ pools.addAll(primaryDataStoreDao.findZoneWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getDataCenterId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
+ if (pools.isEmpty()) {
+ String msg = String.format("Cannot find suitable storage pools in the cluster %s for the conversion", destinationCluster.getName());
+ logger.error(msg);
+ throw new CloudRuntimeException(msg);
+ }
+ poolsList = new ArrayList<>(pools);
+ } else {
+ DataStore dataStore = dataStoreManager.getDataStore(temporaryConvertLocation.getUuid(), temporaryConvertLocation.getRole());
+ poolsList = Collections.singletonList(primaryDataStoreDao.findById(dataStore.getId()));
}
if (serviceOffering.getDiskOfferingId() != null) {
@@ -2062,7 +2183,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
logger.error(msg);
throw new CloudRuntimeException(msg);
}
- if (getStoragePoolWithTags(pools, diskOffering.getTags()) == null) {
+ if (getStoragePoolWithTags(poolsList, diskOffering.getTags()) == null) {
String msg = String.format("Cannot find suitable storage pool for disk offering %s that belongs to the service offering %s", diskOffering.getName(), serviceOffering.getName());
logger.error(msg);
throw new CloudRuntimeException(msg);
@@ -2075,14 +2196,14 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
logger.error(msg);
throw new CloudRuntimeException(msg);
}
- if (getStoragePoolWithTags(pools, diskOffering.getTags()) == null) {
+ if (getStoragePoolWithTags(poolsList, diskOffering.getTags()) == null) {
String msg = String.format("Cannot find suitable storage pool for disk offering %s", diskOffering.getName());
logger.error(msg);
throw new CloudRuntimeException(msg);
}
}
- return pools;
+ return poolsList;
}
private StoragePoolVO getStoragePoolWithTags(List<StoragePoolVO> pools, String tags) {
@@ -2128,39 +2249,63 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
throw new CloudRuntimeException(msg);
}
+ private void checkBeforeSelectingTemporaryConversionStoragePool(StoragePoolVO selectedStoragePool, Long convertStoragePoolId, Cluster destinationCluster, HostVO convertHost) {
+ if (selectedStoragePool == null) {
+ logFailureAndThrowException(String.format("Cannot find a storage pool with ID %s", convertStoragePoolId));
+ }
+ if ((selectedStoragePool.getScope() == ScopeType.CLUSTER && selectedStoragePool.getClusterId() != destinationCluster.getId()) ||
+ (selectedStoragePool.getScope() == ScopeType.ZONE && selectedStoragePool.getDataCenterId() != destinationCluster.getDataCenterId())) {
+ logFailureAndThrowException(String.format("Cannot use the storage pool %s for the instance conversion as " +
+ "it is not in the scope of the cluster %s", selectedStoragePool.getName(), destinationCluster.getName()));
+ }
+ if (convertHost != null && selectedStoragePool.getScope() == ScopeType.CLUSTER && !selectedStoragePool.getClusterId().equals(convertHost.getClusterId())) {
+ logFailureAndThrowException(String.format("Cannot use the storage pool %s for the instance conversion as " +
+ "the host %s for conversion is in a different cluster", selectedStoragePool.getName(), convertHost.getName()));
+ }
+ }
+
+ private DataStoreTO getImageStoreOnDestinationZoneForTemporaryConversion(Cluster destinationCluster, boolean forceConvertToPool) {
+ if (forceConvertToPool) {
+ logFailureAndThrowException("Please select a primary storage pool when the parameter forceconverttopool is set to true");
+ }
+ long zoneId = destinationCluster.getDataCenterId();
+ ImageStoreVO imageStore = imageStoreDao.findOneByZoneAndProtocol(zoneId, "nfs");
+ if (imageStore == null) {
+ logFailureAndThrowException(String.format("Could not find an NFS secondary storage pool on zone %s to use as a temporary location " +
+ "for instance conversion", zoneId));
+ }
+ DataStore dataStore = dataStoreManager.getDataStore(imageStore.getId(), DataStoreRole.Image);
+ return dataStore.getTO();
+ }
+
+ private void checkDestinationOrTemporaryStoragePoolForConversion(StoragePoolVO selectedStoragePool, boolean forceConvertToPool, HostVO convertHost, HostVO importHost) {
+ if (selectedStoragePool.getScope() == ScopeType.HOST && (ObjectUtils.anyNull(convertHost, importHost) ||
+ ObjectUtils.allNotNull(convertHost, importHost) && convertHost.getId() != importHost.getId() ||
+ !forceConvertToPool) ) {
+ logFailureAndThrowException("Please select the same host as convert and importing host and " +
+ "set forceconvertopool to true to use a local storage pool for conversion");
+ }
+ if (!forceConvertToPool && selectedStoragePool.getPoolType() != Storage.StoragePoolType.NetworkFilesystem) {
+ logFailureAndThrowException(String.format("The storage pool %s is not supported for temporary conversion location," +
+ "only NFS storage pools are supported when forceconverttopool is set to false", selectedStoragePool.getName()));
+ }
+ }
+
protected DataStoreTO selectInstanceConversionTemporaryLocation(Cluster destinationCluster,
- HostVO convertHost,
- Long convertStoragePoolId) {
- if (convertStoragePoolId != null) {
- StoragePoolVO selectedStoragePool = primaryDataStoreDao.findById(convertStoragePoolId);
- if (selectedStoragePool == null) {
- logFailureAndThrowException(String.format("Cannot find a storage pool with ID %s", convertStoragePoolId));
- }
- if ((selectedStoragePool.getScope() == ScopeType.CLUSTER && selectedStoragePool.getClusterId() != destinationCluster.getId()) ||
- (selectedStoragePool.getScope() == ScopeType.ZONE && selectedStoragePool.getDataCenterId() != destinationCluster.getDataCenterId())) {
- logFailureAndThrowException(String.format("Cannot use the storage pool %s for the instance conversion as " +
- "it is not in the scope of the cluster %s", selectedStoragePool.getName(), destinationCluster.getName()));
- }
- if (convertHost != null && selectedStoragePool.getScope() == ScopeType.CLUSTER && !selectedStoragePool.getClusterId().equals(convertHost.getClusterId())) {
- logFailureAndThrowException(String.format("Cannot use the storage pool %s for the instance conversion as " +
- "the host %s for conversion is in a different cluster", selectedStoragePool.getName(), convertHost.getName()));
- }
- if (selectedStoragePool.getScope() == ScopeType.HOST) {
- logFailureAndThrowException(String.format("The storage pool %s is a local storage pool and not supported for temporary conversion location, cluster and zone wide NFS storage pools are supported", selectedStoragePool.getName()));
- } else if (selectedStoragePool.getPoolType() != Storage.StoragePoolType.NetworkFilesystem) {
- logFailureAndThrowException(String.format("The storage pool %s is not supported for temporary conversion location, only NFS storage pools are supported", selectedStoragePool.getName()));
- }
- return dataStoreManager.getPrimaryDataStore(convertStoragePoolId).getTO();
- } else {
- long zoneId = destinationCluster.getDataCenterId();
- ImageStoreVO imageStore = imageStoreDao.findOneByZoneAndProtocol(zoneId, "nfs");
- if (imageStore == null) {
- logFailureAndThrowException(String.format("Could not find an NFS secondary storage pool on zone %s to use as a temporary location " +
- "for instance conversion", zoneId));
- }
- DataStore dataStore = dataStoreManager.getDataStore(imageStore.getId(), DataStoreRole.Image);
- return dataStore.getTO();
+ HostVO convertHost, HostVO importHost,
+ Long convertStoragePoolId, boolean forceConvertToPool) {
+ if (convertStoragePoolId == null) {
+ String msg = String.format("No convert storage pool has been provided, " +
+ "selecting an NFS secondary storage pool from the destination cluster (%s) zone", destinationCluster.getName());
+ logger.debug(msg);
+ return getImageStoreOnDestinationZoneForTemporaryConversion(destinationCluster, forceConvertToPool);
}
+
+ StoragePoolVO selectedStoragePool = primaryDataStoreDao.findById(convertStoragePoolId);
+ checkBeforeSelectingTemporaryConversionStoragePool(selectedStoragePool, convertStoragePoolId, destinationCluster, convertHost);
+ checkDestinationOrTemporaryStoragePoolForConversion(selectedStoragePool, forceConvertToPool, convertHost, importHost);
+
+ return dataStoreManager.getPrimaryDataStore(convertStoragePoolId).getTO();
}
protected Map<String, String> createParamsForTemplateFromVmwareVmMigration(String vcenterHost, String datacenterName,
@@ -2186,6 +2331,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
cmdList.add(UnmanageVMInstanceCmd.class);
cmdList.add(ListVmsForImportCmd.class);
cmdList.add(ImportVmCmd.class);
+ cmdList.add(ListImportVMTasksCmd.class);
return cmdList;
}
@@ -2865,7 +3011,9 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
RemoteKvmInstanceDisksCopyTimeout,
ConvertVmwareInstanceToKvmTimeout,
ThreadsOnMSToImportVMwareVMFiles,
- ThreadsOnKVMHostToImportVMwareVMFiles
+ ThreadsOnKVMHostToImportVMwareVMFiles,
+ ConvertVmwareInstanceToKvmExtraParamsAllowed,
+ ConvertVmwareInstanceToKvmExtraParamsAllowedList
};
}
}
diff --git a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml
index 0215abf44b4..3afae7676b7 100644
--- a/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml
+++ b/server/src/main/resources/META-INF/cloudstack/server-compute/spring-server-compute-context.xml
@@ -37,4 +37,6 @@
<bean id="vmImportService" class="org.apache.cloudstack.vm.UnmanagedVMsManagerImpl" />
+ <bean id="importVmTasksManager" class="org.apache.cloudstack.vm.ImportVmTasksManagerImpl" />
+
</beans>
diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
index 99e461949be..465cea3c74a 100644
--- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
+++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java
@@ -39,6 +39,7 @@ import java.util.Map;
import java.util.UUID;
import com.cloud.offering.DiskOffering;
+import com.cloud.vm.ImportVMTaskVO;
import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
@@ -54,6 +55,7 @@ import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationSe
import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager;
+import org.apache.cloudstack.framework.config.ConfigKey;
import org.apache.cloudstack.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
@@ -236,6 +238,8 @@ public class UnmanagedVMsManagerImplTest {
private DataStoreManager dataStoreManager;
@Mock
private StoragePoolHostDao storagePoolHostDao;
+ @Mock
+ private ImportVmTasksManager importVmTasksManager;
@Mock
private VMInstanceVO virtualMachine;
@@ -244,8 +248,15 @@ public class UnmanagedVMsManagerImplTest {
@Mock
DeploymentPlanningManager deploymentPlanningManager;
@Mock
+ ImportVMTaskVO importVMTaskVO;
+ @Mock
private VMInstanceDetailsDao vmInstanceDetailsDao;
+ @Mock
+ private ConfigKey<Boolean> configKeyMockParamsAllowed;
+ @Mock
+ private ConfigKey<String> configKeyMockParamsAllowedList;
+
private static final long virtualMachineId = 1L;
private AutoCloseable closeable;
@@ -720,7 +731,6 @@ public class UnmanagedVMsManagerImplTest {
when(dataStore.getTO()).thenReturn(dataStoreTO);
StoragePoolVO destPool = mock(StoragePoolVO.class);
- when(destPool.getUuid()).thenReturn(UUID.randomUUID().toString());
when(destPool.getDataCenterId()).thenReturn(zoneId);
when(destPool.getClusterId()).thenReturn(null);
when(destPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
@@ -772,6 +782,9 @@ public class UnmanagedVMsManagerImplTest {
when(agentManager.send(Mockito.eq(convertHostId), Mockito.any(CheckConvertInstanceCommand.class))).thenReturn(checkConvertInstanceAnswer);
}
+ when(importVMTaskVO.getId()).thenReturn(1L);
+ when(importVmTasksManager.createImportVMTaskRecord(any(DataCenter.class), any(Account.class), anyLong(), anyString(),
+ anyString(), anyString(), anyString(), any(Host.class), any(Host.class))).thenReturn(importVMTaskVO);
when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any(DiskOffering.class))).thenReturn(true);
ConvertInstanceAnswer convertInstanceAnswer = mock(ConvertInstanceAnswer.class);
@@ -895,7 +908,7 @@ public class UnmanagedVMsManagerImplTest {
long poolId = 1L;
when(primaryDataStoreDao.findById(poolId)).thenReturn(null);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
}
@Test(expected = CloudRuntimeException.class)
@@ -906,7 +919,7 @@ public class UnmanagedVMsManagerImplTest {
when(pool.getScope()).thenReturn(ScopeType.CLUSTER);
when(pool.getClusterId()).thenReturn(100L);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
}
@@ -920,7 +933,7 @@ public class UnmanagedVMsManagerImplTest {
HostVO host = mock(HostVO.class);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(host.getClusterId()).thenReturn(2L);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, host, poolId);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, host, null, poolId, false);
}
@@ -931,7 +944,7 @@ public class UnmanagedVMsManagerImplTest {
StoragePoolVO pool = mock(StoragePoolVO.class);
when(pool.getScope()).thenReturn(ScopeType.HOST);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
}
@Test(expected = CloudRuntimeException.class)
@@ -943,14 +956,14 @@ public class UnmanagedVMsManagerImplTest {
when(pool.getClusterId()).thenReturn(1L);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
}
@Test(expected = CloudRuntimeException.class)
public void testSelectInstanceConversionTemporaryLocationNoPoolAvailable() {
ClusterVO cluster = getClusterForTests();
when(imageStoreDao.findOneByZoneAndProtocol(anyLong(), anyString())).thenReturn(null);
- unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null);
+ unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, null, false);
}
@Test
@@ -1220,4 +1233,81 @@ public class UnmanagedVMsManagerImplTest {
Assert.fail("Exception encountered: " + e.getMessage());
}
}
+
+ @Test
+ public void testCheckConversionStoragePoolSecondaryStorageStaging() {
+ unmanagedVMsManager.checkConversionStoragePool(null, false);
+ Mockito.verifyNoInteractions(primaryDataStoreDao);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testCheckConversionStoragePoolTemporarySecondaryStorageForceConvertToPool() {
+ unmanagedVMsManager.checkConversionStoragePool(null, true);
+ }
+
+ @Test
+ public void testCheckConversionStoragePoolPrimaryStagingPool() {
+ StoragePoolVO destPool = mock(StoragePoolVO.class);
+ long destPoolId = 1L;
+ Mockito.when(primaryDataStoreDao.findById(destPoolId)).thenReturn(destPool);
+ unmanagedVMsManager.checkConversionStoragePool(destPoolId, false);
+ }
+
+ @Test
+ public void testCheckConversionStoragePoolPrimaryStagingPoolTypeAllowedForce() {
+ StoragePoolVO destPool = mock(StoragePoolVO.class);
+ Mockito.when(destPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem);
+ long destPoolId = 1L;
+ Mockito.when(primaryDataStoreDao.findById(destPoolId)).thenReturn(destPool);
+ unmanagedVMsManager.checkConversionStoragePool(destPoolId, true);
+ }
+
+ @Test(expected = CloudRuntimeException.class)
+ public void testCheckConversionStoragePoolPrimaryStagingPoolTypeNotAllowedForce() {
+ StoragePoolVO destPool = mock(StoragePoolVO.class);
+ Mockito.when(destPool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD);
+ long destPoolId = 1L;
+ Mockito.when(primaryDataStoreDao.findById(destPoolId)).thenReturn(destPool);
+ unmanagedVMsManager.checkConversionStoragePool(destPoolId, true);
+ }
+
+ @Test
+ public void testCheckExtraParamsAllowedEmptyParams() {
+ unmanagedVMsManager.checkExtraParamsAllowed(null);
+ Mockito.verifyNoInteractions(configKeyMockParamsAllowed);
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testCheckExtraParamsAllowedDisabledByAdministrator() {
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowed = configKeyMockParamsAllowed;
+ Mockito.when(configKeyMockParamsAllowed.value()).thenReturn(false);
+ unmanagedVMsManager.checkExtraParamsAllowed("--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254 -x");
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testCheckExtraParamsAllowedEnabledButEmptyAllowedList() {
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowed = configKeyMockParamsAllowed;
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowedList = configKeyMockParamsAllowedList;
+ Mockito.when(configKeyMockParamsAllowed.value()).thenReturn(true);
+ Mockito.when(configKeyMockParamsAllowedList.value()).thenReturn(null);
+ unmanagedVMsManager.checkExtraParamsAllowed("--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254 -x");
+ }
+
+ @Test
+ public void testCheckExtraParamsAllowedEnabledAndAllowedList() {
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowed = configKeyMockParamsAllowed;
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowedList = configKeyMockParamsAllowedList;
+ Mockito.when(configKeyMockParamsAllowed.value()).thenReturn(true);
+ Mockito.when(configKeyMockParamsAllowedList.value()).thenReturn("mac,network,x");
+ unmanagedVMsManager.checkExtraParamsAllowed("--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254 -x");
+ }
+
+ @Test(expected = ServerApiException.class)
+ public void testCheckExtraParamsAllowedEnabledParamNotInTheAllowedList() {
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowed = configKeyMockParamsAllowed;
+ unmanagedVMsManager.ConvertVmwareInstanceToKvmExtraParamsAllowedList = configKeyMockParamsAllowedList;
+ Mockito.when(configKeyMockParamsAllowed.value()).thenReturn(true);
+ Mockito.when(configKeyMockParamsAllowedList.value()).thenReturn("network,x");
+ unmanagedVMsManager.checkExtraParamsAllowed("--mac 00:0c:29:e6:3d:9d:ip:192.168.0.89,192.168.0.1,24,192.168.0.254 -x");
+ }
}
diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py
index ad72b4c7938..e41a04ff2e1 100644
--- a/tools/apidoc/gen_toc.py
+++ b/tools/apidoc/gen_toc.py
@@ -272,7 +272,8 @@ known_categories = {
'Extension' : 'Extension',
'Extensions' : 'Extension',
'CustomAction' : 'Extension',
- 'CustomActions' : 'Extension'
+ 'CustomActions' : 'Extension',
+ 'ImportVmTask': 'Import VM Task'
}
diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json
index 3d00580dceb..a877b9f7c0d 100644
--- a/ui/public/locales/en.json
+++ b/ui/public/locales/en.json
@@ -1089,6 +1089,7 @@
"label.forcks": "For CKS",
"label.forbidden": "Forbidden",
"label.forced": "Force",
+"label.force.convert.to.pool": "Force converting to storage pool directly (not using temporary storage for conversion)",
"label.force.ms.to.import.vm.files": "Enable to force OVF Download via Management Server. Disable to use KVM Host ovftool (if installed)",
"label.force.update.os.type": "Force update OS type",
"label.force.stop": "Force stop",
@@ -3732,8 +3733,10 @@
"message.select.deselect.desired.options": "Please select / deselect the desired options",
"message.select.deselect.to.sort": "Please select / deselect to sort the values",
"message.select.destination.image.stores": "Please select Image Store(s) to which data is to be migrated to",
+"message.select.destination.storage.instance.conversion": "(Optional) Select a Primary Storage destination for the converted disks",
"message.select.disk.offering": "Please select a disk offering for disk",
"message.select.end.date.and.time": "Select an end date & time.",
+"message.select.extra.parameters.for.instance.conversion": "(Optional) Pass extra parameters to the virt-v2v command on the conversion host",
"message.select.kvm.host.instance.conversion": "(Optional) Select a KVM host in the Zone to perform the instance conversion through virt-v2v",
"message.select.kvm.host.instance.import": "(Optional) Select a KVM host in the Cluster to perform the importing of the converted instance",
"message.select.load.balancer.rule": "Please select a load balancer rule for your AutoScale Instance group.",
@@ -3741,7 +3744,7 @@
"message.select.nic.network": "Please select a Network for NIC",
"message.select.security.groups": "Please select security group(s) for your new Instance.",
"message.select.start.date.and.time": "Select a start date & time.",
-"message.select.temporary.storage.instance.conversion": "(Optional) Select a Storage temporary destination for the converted disks through virt-v2v",
+"message.select.temporary.storage.instance.conversion": "(Optional) Select a staging storage for the converted disks",
"message.select.volume.to.continue": "Please select a volume to continue.",
"message.select.vm.to.continue": "Please select an Instance to continue.",
"message.select.zone.description": "Select type of Zone basic/advanced.",
diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue
index 4c1a3d53fbc..8cc27b2a86d 100644
--- a/ui/src/views/tools/ImportUnmanagedInstance.vue
+++ b/ui/src/views/tools/ImportUnmanagedInstance.vue
@@ -152,6 +152,12 @@
</a-row>
</a-radio-group>
</a-form-item>
+ <a-form-item name="forceconverttopool" ref="forceconverttopool" v-if="selectedVmwareVcenter">
+ <template #label>
+ <tooltip-label :title="$t('label.force.convert.to.pool')" :tooltip="apiParams.forceconverttopool.description"/>
+ </template>
+ <a-switch v-model:checked="form.forceconverttopool" @change="onForceConvertToPoolChange" />
+ </a-form-item>
<a-form-item name="converthostid" ref="converthostid">
<check-box-select-pair
layout="vertical"
@@ -179,17 +185,21 @@
<a-form-item name="convertstorageoption" ref="convertstorageoption">
<check-box-select-pair
layout="vertical"
- style="margin-bottom: 5px"
v-if="cluster.hypervisortype === 'KVM' && selectedVmwareVcenter"
:resourceKey="cluster.id"
:selectOptions="storageOptionsForConversion"
- :checkBoxLabel="$t('message.select.temporary.storage.instance.conversion')"
+ :checkBoxLabel="switches.forceConvertToPool ? $t('message.select.destination.storage.instance.conversion') : $t('message.select.temporary.storage.instance.conversion')"
:defaultCheckBoxValue="false"
:reversed="false"
@handle-checkselectpair-change="updateSelectedStorageOptionForConversion"
/>
</a-form-item>
- <a-form-item v-if="showStoragePoolsForConversion" name="convertstoragepool" ref="convertstoragepool" :label="$t('label.storagepool')">
+ <a-form-item
+ v-if="showStoragePoolsForConversion"
+ name="convertstoragepool"
+ ref="convertstoragepool"
+ :label="$t('label.storagepool')"
+ >
<a-select
v-model:value="form.convertstoragepoolid"
defaultActiveFirstOption
@@ -204,6 +214,18 @@
</a-select-option>
</a-select>
</a-form-item>
+ <a-form-item name="extraparams" ref="extraparams">
+ <a-checkbox
+ v-if="cluster.hypervisortype === 'KVM' && selectedVmwareVcenter && vmwareToKvmExtraParamsAllowed"
+ v-model:checked="vmwareToKvmExtraParamsSelected">
+ {{ $t('message.select.extra.parameters.for.instance.conversion') }}
+ </a-checkbox>
+ <a-input
+ v-if="vmwareToKvmExtraParamsSelected"
+ v-model:value="vmwareToKvmExtraParams"
+ :placeholder="$t('label.extra')"
+ />
+ </a-form-item>
<a-form-item name="forcemstoimportvmfiles" ref="forcemstoimportvmfiles" v-if="selectedVmwareVcenter">
<template #label>
<tooltip-label :title="$t('label.force.ms.to.import.vm.files')" :tooltip="apiParams.forcemstoimportvmfiles.description"/>
@@ -529,7 +551,10 @@ export default {
title: this.$t('label.rootdisk')
}
],
- selectedRootDiskSources: []
+ selectedRootDiskSources: [],
+ vmwareToKvmExtraParamsAllowed: false,
+ vmwareToKvmExtraParamsSelected: false,
+ vmwareToKvmExtraParams: ''
}
},
beforeCreate () {
@@ -724,6 +749,7 @@ export default {
migrateallowed: this.switches.migrateAllowed,
forced: this.switches.forced,
forcemstoimportvmfiles: this.switches.forceMsToImportVmFiles,
+ forceconverttopool: this.switches.forceConvertToPool,
domainid: null,
account: null
})
@@ -749,6 +775,20 @@ export default {
if (this.resource?.disk?.length > 1) {
this.updateSelectedRootDisk()
}
+ this.fetchVmwareToKVMExtraConfigsSetting()
+ },
+ fetchVmwareToKVMExtraConfigsSetting () {
+ const params = {
+ name: 'convert.vmware.instance.to.kvm.extra.params.allowed'
+ }
+ getAPI('listConfigurations', params).then(json => {
+ if (json.listconfigurationsresponse.configuration !== null) {
+ const config = json.listconfigurationsresponse.configuration[0]
+ if (config && config.name === params.name) {
+ this.vmwareToKvmExtraParamsAllowed = config.value
+ }
+ }
+ })
},
getMeta (obj, metaKeys) {
var meta = []
@@ -1027,20 +1067,23 @@ export default {
}
},
resetStorageOptionsForConversion () {
- this.storageOptionsForConversion = [
- {
- id: 'secondary',
- name: 'Secondary Storage'
- }, {
- id: 'primary',
- name: 'Primary Storage'
- }
- ]
+ this.storageOptionsForConversion = this.switches.forceConvertToPool ? [] : [{
+ id: 'secondary',
+ name: 'Secondary Storage'
+ }]
+ this.storageOptionsForConversion.push({
+ id: 'primary',
+ name: 'Primary Storage'
+ })
},
onSelectRootDisk (val) {
this.selectedRootDiskIndex = val
this.updateSelectedRootDisk()
},
+ onForceConvertToPoolChange (val) {
+ this.switches.forceConvertToPool = val
+ this.resetStorageOptionsForConversion()
+ },
updateSelectedRootDisk () {
var rootDisk = this.resource.disk[this.selectedRootDiskIndex]
rootDisk.size = rootDisk.capacity / (1024 * 1024 * 1024)
@@ -1156,7 +1199,13 @@ export default {
if (this.selectedStoragePoolForConversion) {
params.convertinstancepoolid = this.selectedStoragePoolForConversion
}
+ if (this.vmwareToKvmExtraParams) {
+ params.extraparams = this.vmwareToKvmExtraParams
+ }
params.forcemstoimportvmfiles = values.forcemstoimportvmfiles
+ if (values.forceconverttopool) {
+ params.forceconverttopool = values.forceconverttopool
+ }
}
var keys = ['hostname', 'domainid', 'projectid', 'account', 'migrateallowed', 'forced', 'forcemstoimportvmfiles']
if (this.templateType !== 'auto') {
diff --git a/ui/src/views/tools/ImportVmTasks.vue b/ui/src/views/tools/ImportVmTasks.vue
new file mode 100644
index 00000000000..51741f18e69
--- /dev/null
+++ b/ui/src/views/tools/ImportVmTasks.vue
@@ -0,0 +1,150 @@
+// 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.
+
+<template>
+ <a-card class="instances-card">
+ <template #title>
+ Import VM Tasks
+ <a-tooltip :title="'Running Import VM Tasks'">
+ <info-circle-outlined />
+ </a-tooltip>
+ <a-button
+ style="margin-left: 12px; margin-top: 4px"
+ :loading="loading"
+ size="small"
+ shape="round"
+ @click="fetchData()">
+ <template #icon><reload-outlined /></template>
+ </a-button>
+ <a-select
+ :placeholder="$t('label.filterby')"
+ :value="importVmTasksFilterValue"
+ style="min-width: 100px; margin-left: 10px; margin-bottom: 5px"
+ size=small
+ @change="onFilterChange"
+ showSearch
+ optionFilterProp="label"
+ :filterOption="(input, option) => {
+ return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
+ }"
+ >
+ <template #suffixIcon><filter-outlined class="ant-select-suffix" /></template>
+ <a-select-option
+ v-for="filter in filters"
+ :key="filter"
+ :label="$t('label.' + filter)"
+ >
+ {{ $t('label.' + filter) }}
+ </a-select-option>
+ </a-select>
+ </template>
+ <a-table
+ :data-source="tasks"
+ :columns="columns">
+ <template #bodyCell="{ column, record }">
+ <template v-if="column.key === 'convertinstancehostid'">
+ <router-link :to="{ path: '/host/' + record.convertinstancehostid }">{{ record.convertinstancehostname }}</router-link>
+ </template>
+ <template v-else-if="column.key === 'displayname'">
+ <router-link v-if="record.virtualmachineid" :to="{ path: '/vm/' + record.virtualmachineid }">{{ record.displayname }}</router-link>
+ <span v-else>{{ record.displayname }}</span>
+ </template>
+ </template>
+ </a-table>
+ </a-card>
+</template>
+
+<script>
+export default {
+ name: 'ImportVmTasks',
+ components: {
+ },
+ props: {
+ tasks: {
+ type: Array,
+ required: true
+ },
+ loading: {
+ type: Boolean,
+ required: false
+ }
+ },
+ data () {
+ const columns = [
+ {
+ key: 'displayname',
+ title: 'VM Display Name',
+ dataIndex: 'displayname'
+ },
+ {
+ key: 'convertinstancehostid',
+ title: 'Conversion Host',
+ dataIndex: 'convertinstancehostid'
+ },
+ {
+ key: 'step',
+ title: 'Current Step',
+ dataIndex: 'step'
+ },
+ {
+ key: 'stepduration',
+ title: 'Current Step Duration',
+ dataIndex: 'stepduration'
+ },
+ {
+ key: 'description',
+ title: 'Description',
+ dataIndex: 'description'
+ },
+ {
+ key: 'duration',
+ title: 'Total Duration',
+ dataIndex: 'duration'
+ },
+ {
+ key: 'sourcevmname',
+ title: 'Source VM Name',
+ dataIndex: 'sourcevmname'
+ },
+ {
+ key: 'vcenter',
+ title: 'vCenter',
+ dataIndex: 'vcenter'
+ },
+ {
+ key: 'datacentername',
+ title: 'Datacenter Name',
+ dataIndex: 'datacentername'
+ }
+ ]
+ return {
+ columns,
+ filters: ['running', 'completed'],
+ filterValue: 'running'
+ }
+ },
+ methods: {
+ fetchData () {
+ this.$emit('fetch-import-vm-tasks', this.filterValue)
+ },
+ onFilterChange (e) {
+ this.filterValue = e
+ this.fetchData()
+ }
+ }
+}
+</script>
diff --git a/ui/src/views/tools/ManageInstances.vue b/ui/src/views/tools/ManageInstances.vue
index b6f3c50e267..d58716df3aa 100644
--- a/ui/src/views/tools/ManageInstances.vue
+++ b/ui/src/views/tools/ManageInstances.vue
@@ -333,166 +333,179 @@
</a-col>
</a-row>
<a-divider />
- <a-row :gutter="12">
- <a-col v-if="!isDiskImport" :md="24" :lg="(!isMigrateFromVmware && showManagedInstances) ? 12 : 24">
- <a-card class="instances-card">
- <template #title>
- {{ (isMigrateFromVmware && vmwareVcenterType === 'existing') ? $t('label.instances') : $t('label.unmanaged.instances') }}
- <a-tooltip :title="(isMigrateFromVmware && vmwareVcenterType === 'existing') ? $t('message.instances.migrate.vmware') : $t('message.instances.unmanaged')">
- <info-circle-outlined />
- </a-tooltip>
- <a-button
- style="margin-left: 12px; margin-top: 4px"
- :loading="unmanagedInstancesLoading"
- size="small"
- shape="round"
- @click="fetchUnmanagedInstances()" >
- <template #icon><reload-outlined /></template>
- </a-button>
- <span style="float: right; width: 50%">
- <search-view
- :searchFilters="searchFilters.unmanaged"
- :searchParams="searchParams.unmanaged"
- :apiName="listInstancesApi.unmanaged"
- @search="searchUnmanagedInstances"
- />
- </span>
- </template>
- <a-table
- v-if="!isExternal"
- class="instances-card-table"
- :loading="unmanagedInstancesLoading"
- :rowSelection="unmanagedInstanceSelection"
- :rowKey="(record, index) => index"
- :columns="unmanagedInstancesColumns"
- :data-source="unmanagedInstances"
- :pagination="false"
- size="middle"
- :rowClassName="getRowClassName"
- >
- <template #bodyCell="{ column, text }">
- <template v-if="column.key === 'state'">
- <status :text="text ? text : ''" displayText />
+
+ <a-tabs v-model:activeKey="activeTabKey" @change="onTabChange">
+ <a-tab-pane :key=1 tab="Instances Listing">
+ <a-row :gutter="12">
+ <a-col v-if="!isDiskImport" :md="24" :lg="(!isMigrateFromVmware && showManagedInstances) ? 12 : 24">
+ <a-card class="instances-card">
+ <template #title>
+ {{ (isMigrateFromVmware && vmwareVcenterType === 'existing') ? $t('label.instances') : $t('label.unmanaged.instances') }}
+ <a-tooltip :title="(isMigrateFromVmware && vmwareVcenterType === 'existing') ? $t('message.instances.migrate.vmware') : $t('message.instances.unmanaged')">
+ <info-circle-outlined />
+ </a-tooltip>
+ <a-button
+ style="margin-left: 12px; margin-top: 4px"
+ :loading="unmanagedInstancesLoading"
+ size="small"
+ shape="round"
+ @click="fetchUnmanagedInstances()" >
+ <template #icon><reload-outlined /></template>
+ </a-button>
+ <span style="float: right; width: 50%">
+ <search-view
+ :searchFilters="searchFilters.unmanaged"
+ :searchParams="searchParams.unmanaged"
+ :apiName="listInstancesApi.unmanaged"
+ @search="searchUnmanagedInstances"
+ />
+ </span>
</template>
- </template>
- </a-table>
- <a-table
- v-if="isExternal"
- class="instances-card-table"
- :loading="unmanagedInstancesLoading"
- :rowSelection="unmanagedInstanceSelection"
- :rowKey="(record, index) => index"
- :columns="externalInstancesColumns"
- :data-source="unmanagedInstances"
- :pagination="false"
- size="middle"
- :rowClassName="getRowClassName"
- >
- <template #bodyCell="{ column, text }">
- <template v-if="column.key === 'state'">
+ <a-table
+ v-if="!isExternal"
+ class="instances-card-table"
+ :loading="unmanagedInstancesLoading"
+ :rowSelection="unmanagedInstanceSelection"
+ :rowKey="(record, index) => index"
+ :columns="unmanagedInstancesColumns"
+ :data-source="unmanagedInstances"
+ :pagination="false"
+ size="middle"
+ :rowClassName="getRowClassName"
+ >
+ <template #bodyCell="{ column, text }">
+ <template v-if="column.key === 'state'">
<status :text="text ? text : ''" displayText />
+ </template>
</template>
- </template>
- </a-table>
- <div class="instances-card-footer">
- <a-pagination
- class="row-element"
- size="small"
- :current="page.unmanaged"
- :pageSize="pageSize.unmanaged"
- :total="itemCount.unmanaged"
- :showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.unmanaged-1)*pageSize.unmanaged))}-${Math.min(page.unmanaged*pageSize.unmanaged, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
- @change="fetchUnmanagedInstances"
- showQuickJumper>
- <template #buildOptionText="props">
- <span>{{ props.value }} / {{ $t('label.page') }}</span>
- </template>
- </a-pagination>
- <div :span="24" class="action-button-right">
- <a-button
- :loading="importUnmanagedInstanceLoading"
- :disabled="!(('importUnmanagedInstance' in $store.getters.apis) && unmanagedInstancesSelectedRowKeys.length > 0)"
- type="primary"
- @click="onManageInstanceAction">
- <template #icon><import-outlined /></template>
- {{ $t('label.import.instance') }}
- </a-button>
- </div>
- </div>
- </a-card>
- </a-col>
- <a-col :md="24" :lg="12" v-if="!isMigrateFromVmware && showManagedInstances">
- <a-card class="instances-card">
- <template #title>
- {{ $t('label.managed.instances') }}
- <a-tooltip :title="$t('message.instances.managed')">
- <info-circle-outlined />
- </a-tooltip>
- <a-button
- style="margin-left: 12px; margin-top: 4px"
- :loading="managedInstancesLoading"
- size="small"
- shape="round"
- @click="fetchManagedInstances()" >
- <template #icon><reload-outlined /></template>
- </a-button>
- <span style="float: right; width: 50%">
- <search-view
- :searchFilters="searchFilters.managed"
- :searchParams="searchParams.managed"
- :apiName="listInstancesApi.managed"
- @search="searchManagedInstances"
- />
- </span>
- </template>
- <a-table
- class="instances-card-table"
- :loading="managedInstancesLoading"
- :rowSelection="managedInstanceSelection"
- :rowKey="(record, index) => index"
- :columns="managedInstancesColumns"
- :data-source="managedInstances"
- :pagination="false"
- size="middle"
- :rowClassName="getRowClassName"
- >
- <template #bodyCell="{ column, text, record }">
- <template v-if="column.key === 'name'">
- <router-link :to="{ path: '/vm/' + record.id }">{{ text }}</router-link>
- </template>
- <template v-if="column.key === 'state'">
- <status :text="text ? text : ''" displayText />
- </template>
- </template>
- </a-table>
- <div class="instances-card-footer">
- <a-pagination
- class="row-element"
- size="small"
- :current="page.managed"
- :pageSize="pageSize.managed"
- :total="itemCount.managed"
- :showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.managed-1)*pageSize.managed))}-${Math.min(page.managed*pageSize.managed, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
- @change="fetchManagedInstances"
- showQuickJumper>
- <template #buildOptionText="props">
- <span>{{ props.value }} / {{ $t('label.page') }}</span>
+ </a-table>
+ <a-table
+ v-if="isExternal"
+ class="instances-card-table"
+ :loading="unmanagedInstancesLoading"
+ :rowSelection="unmanagedInstanceSelection"
+ :rowKey="(record, index) => index"
+ :columns="externalInstancesColumns"
+ :data-source="unmanagedInstances"
+ :pagination="false"
+ size="middle"
+ :rowClassName="getRowClassName"
+ >
+ <template #bodyCell="{ column, text }">
+ <template v-if="column.key === 'state'">
+ <status :text="text ? text : ''" displayText />
+ </template>
+ </template>
+ </a-table>
+ <div class="instances-card-footer">
+ <a-pagination
+ class="row-element"
+ size="small"
+ :current="page.unmanaged"
+ :pageSize="pageSize.unmanaged"
+ :total="itemCount.unmanaged"
+ :showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.unmanaged-1)*pageSize.unmanaged))}-${Math.min(page.unmanaged*pageSize.unmanaged, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
+ @change="fetchUnmanagedInstances"
+ showQuickJumper>
+ <template #buildOptionText="props">
+ <span>{{ props.value }} / {{ $t('label.page') }}</span>
+ </template>
+ </a-pagination>
+ <div :span="24" class="action-button-right">
+ <a-button
+ :loading="importUnmanagedInstanceLoading"
+ :disabled="!(('importUnmanagedInstance' in $store.getters.apis) && unmanagedInstancesSelectedRowKeys.length > 0)"
+ type="primary"
+ @click="onManageInstanceAction">
+ <template #icon><import-outlined /></template>
+ {{ $t('label.import.instance') }}
+ </a-button>
+ </div>
+ </div>
+ </a-card>
+ </a-col>
+ <a-col :md="24" :lg="12" v-if="!isMigrateFromVmware && showManagedInstances">
+ <a-card class="instances-card">
+ <template #title>
+ {{ $t('label.managed.instances') }}
+ <a-tooltip :title="$t('message.instances.managed')">
+ <info-circle-outlined />
+ </a-tooltip>
+ <a-button
+ style="margin-left: 12px; margin-top: 4px"
+ :loading="managedInstancesLoading"
+ size="small"
+ shape="round"
+ @click="fetchManagedInstances()" >
+ <template #icon><reload-outlined /></template>
+ </a-button>
+ <span style="float: right; width: 50%">
+ <search-view
+ :searchFilters="searchFilters.managed"
+ :searchParams="searchParams.managed"
+ :apiName="listInstancesApi.managed"
+ @search="searchManagedInstances"
+ />
+ </span>
</template>
- </a-pagination>
- <div :span="24" class="action-button-right">
- <a-button
+ <a-table
+ class="instances-card-table"
+ :loading="managedInstancesLoading"
+ :rowSelection="managedInstanceSelection"
+ :rowKey="(record, index) => index"
+ :columns="managedInstancesColumns"
+ :data-source="managedInstances"
+ :pagination="false"
+ size="middle"
+ :rowClassName="getRowClassName"
+ >
+ <template #bodyCell="{ column, text, record }">
+ <template v-if="column.key === 'name'">
+ <router-link :to="{ path: '/vm/' + record.id }">{{ text }}</router-link>
+ </template>
+ <template v-if="column.key === 'state'">
+ <status :text="text ? text : ''" displayText />
+ </template>
+ </template>
+ </a-table>
+ <div class="instances-card-footer">
+ <a-pagination
+ class="row-element"
+ size="small"
+ :current="page.managed"
+ :pageSize="pageSize.managed"
+ :total="itemCount.managed"
+ :showTotal="total => `${$t('label.showing')} ${Math.min(total, 1+((page.managed-1)*pageSize.managed))}-${Math.min(page.managed*pageSize.managed, total)} ${$t('label.of')} ${total} ${$t('label.items')}`"
+ @change="fetchManagedInstances"
+ showQuickJumper>
+ <template #buildOptionText="props">
+ <span>{{ props.value }} / {{ $t('label.page') }}</span>
+ </template>
+ </a-pagination>
+ <div :span="24" class="action-button-right">
+ <a-button
+
+ :disabled="!(('unmanageVirtualMachine' in $store.getters.apis) && managedInstancesSelectedRowKeys.length > 0)"
+ type="primary"
+ @click="onUnmanageInstanceAction">
+ <template #icon><disconnect-outlined /></template>
+ {{ managedInstancesSelectedRowKeys.length > 1 ? $t('label.action.unmanage.instances') : $t('label.action.unmanage.instance') }}
+ </a-button>
+ </div>
+ </div>
+ </a-card>
+ </a-col>
+ </a-row>
+ </a-tab-pane>
+ <a-tab-pane :key=2 tab="Import VM Tasks" v-if="isMigrateFromVmware">
+ <ImportVmTasks
+ :tasks="importVmTasks"
+ :loading="loadingImportVmTasks"
+ @fetch-import-vm-tasks="fetchImportVmTasks"
+ />
+ </a-tab-pane>
+ </a-tabs>
- :disabled="!(('unmanageVirtualMachine' in $store.getters.apis) && managedInstancesSelectedRowKeys.length > 0)"
- type="primary"
- @click="onUnmanageInstanceAction">
- <template #icon><disconnect-outlined /></template>
- {{ managedInstancesSelectedRowKeys.length > 1 ? $t('label.action.unmanage.instances') : $t('label.action.unmanage.instance') }}
- </a-button>
- </div>
- </div>
- </a-card>
- </a-col>
- </a-row>
</a-card>
<a-modal
@@ -546,6 +559,7 @@ import ImportUnmanagedInstances from '@/views/tools/ImportUnmanagedInstance'
import ResourceIcon from '@/components/view/ResourceIcon'
import SelectVmwareVcenter from '@/views/tools/SelectVmwareVcenter'
import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
+import ImportVmTasks from '@/views/tools/ImportVmTasks.vue'
export default {
components: {
@@ -555,7 +569,8 @@ export default {
SearchView,
ImportUnmanagedInstances,
ResourceIcon,
- SelectVmwareVcenter
+ SelectVmwareVcenter,
+ ImportVmTasks
},
name: 'ManageVms',
data () {
@@ -747,7 +762,10 @@ export default {
selectedUnmanagedInstance: {},
query: {},
vmwareVcenterType: undefined,
- selectedVmwareVcenter: undefined
+ selectedVmwareVcenter: undefined,
+ activeTabKey: 1,
+ loadingImportVmTasks: false,
+ importVmTasks: []
}
},
created () {
@@ -1084,6 +1102,7 @@ export default {
this.page.managed = 1
this.managedInstances = []
this.managedInstancesSelectedRowKeys = []
+ this.activeTabKey = 1
},
onSelectHypervisor (value) {
this.sourceHypervisor = value
@@ -1154,6 +1173,27 @@ export default {
this.updateQuery('scope', value)
this.fetchOptions(this.params.pools, 'pools', value)
},
+ onTabChange (e) {
+ if (e === 2) {
+ this.fetchImportVmTasks()
+ }
+ },
+ fetchImportVmTasks (filter) {
+ this.loadingImportVmTasks = true
+ const params = {
+ zoneid: this.zoneId
+ }
+ if (filter && filter === 'completed') {
+ params.showcompleted = true
+ }
+ getAPI('listImportVmTasks', params).then(response => {
+ this.importVmTasks = response.listimportvmtasksresponse.importvmtask || []
+ }).catch(error => {
+ this.$notifyError(error)
+ }).finally(() => {
+ this.loadingImportVmTasks = false
+ })
+ },
fetchInstances () {
this.fetchUnmanagedInstances()
if (this.isUnmanaged) {
diff --git a/utils/src/main/java/com/cloud/utils/script/Script.java b/utils/src/main/java/com/cloud/utils/script/Script.java
index 28ae3264355..b56a457aeaa 100644
--- a/utils/src/main/java/com/cloud/utils/script/Script.java
+++ b/utils/src/main/java/com/cloud/utils/script/Script.java
@@ -244,6 +244,7 @@ public class Script implements Callable<String> {
try {
_logger.trace(String.format("Creating process for command [%s].", commandLine));
+
ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true);
if (_workDir != null)