From b106d6e190b098ee1e97264952ed59a277e5c360 Mon Sep 17 00:00:00 2001 From: Nicolas Vazquez Date: Fri, 10 Oct 2025 20:00:29 -0300 Subject: [PATCH] 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 <175728472+Copilot@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 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Suresh Kumar Anaparti --- agent/conf/agent.properties | 6 + .../agent/properties/AgentProperties.java | 14 + .../apache/cloudstack/api/ApiConstants.java | 4 + .../api/command/admin/vm/ImportVmCmd.java | 20 + .../admin/vm/ListImportVMTasksCmd.java | 123 ++++++ .../api/response/ImportVMTaskResponse.java | 245 ++++++++++++ .../apache/cloudstack/vm/ImportVmTask.java | 28 ++ .../cloudstack/vm/ImportVmTasksManager.java | 38 ++ .../agent/api/ConvertInstanceCommand.java | 17 +- .../api/ImportConvertedInstanceCommand.java | 9 +- .../java/com/cloud/vm/ImportVMTaskVO.java | 259 +++++++++++++ .../com/cloud/vm/dao/ImportVMTaskDao.java | 29 ++ .../com/cloud/vm/dao/ImportVMTaskDaoImpl.java | 65 ++++ ...spring-engine-schema-core-daos-context.xml | 1 + .../META-INF/db/schema-42100to42200.sql | 30 ++ .../resource/LibvirtComputingResource.java | 26 ++ .../LibvirtConvertInstanceCommandWrapper.java | 73 ++-- ...ImportConvertedInstanceCommandWrapper.java | 16 +- ...virtConvertInstanceCommandWrapperTest.java | 23 +- .../vm/ImportVmTasksManagerImpl.java | 225 +++++++++++ .../vm/UnmanagedVMsManagerImpl.java | 278 ++++++++++---- .../spring-server-compute-context.xml | 2 + .../vm/UnmanagedVMsManagerImplTest.java | 104 ++++- tools/apidoc/gen_toc.py | 3 +- ui/public/locales/en.json | 5 +- .../views/tools/ImportUnmanagedInstance.vue | 75 +++- ui/src/views/tools/ImportVmTasks.vue | 150 ++++++++ ui/src/views/tools/ManageInstances.vue | 362 ++++++++++-------- .../java/com/cloud/utils/script/Script.java | 1 + 29 files changed, 1950 insertions(+), 281 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListImportVMTasksCmd.java create mode 100644 api/src/main/java/org/apache/cloudstack/api/response/ImportVMTaskResponse.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/ImportVmTask.java create mode 100644 api/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManager.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/ImportVMTaskVO.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDao.java create mode 100644 engine/schema/src/main/java/com/cloud/vm/dao/ImportVMTaskDaoImpl.java create mode 100644 server/src/main/java/org/apache/cloudstack/vm/ImportVmTasksManagerImpl.java create mode 100644 ui/src/views/tools/ImportVmTasks.vue 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 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.
+ * Default value: null + */ + public static final Property 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.
+ * Default value: null + */ + public static final Property CONVERT_ENV_VIRTV2V_TMPDIR = new Property<>("convert.instance.env.virtv2v.tmpdir", null, String.class); + /** * BGP controll CIDR * Data type: String.
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 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 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 destinationStoragePools; private DataStoreTO conversionTemporaryLocation; private String temporaryConvertUuid; + private boolean forceConvertToPool; public ImportConvertedInstanceCommand() { } public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance, List 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 { + + List 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 implements ImportVMTaskDao { + + private SearchBuilder AllFieldsSearch; + + public ImportVMTaskDaoImpl() { + } + + @PostConstruct + void init() { + AllFieldsSearch = createSearchBuilder(); + AllFieldsSearch.and("zoneId", AllFieldsSearch.entity().getZoneId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("vcenter", AllFieldsSearch.entity().getVcenter(), SearchCriteria.Op.EQ); + AllFieldsSearch.and("convertHostId", AllFieldsSearch.entity().getConvertHostId(), SearchCriteria.Op.EQ); + AllFieldsSearch.done(); + } + + + @Override + public List listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted) { + SearchCriteria sc = AllFieldsSearch.create(); + if (zoneId != null) { + sc.setParameters("zoneId", zoneId); + } + if (accountId != null) { + sc.setParameters("accountId", accountId); + } + if (StringUtils.isNotBlank(vcenter)) { + sc.setParameters("vcenter", vcenter); + } + if (convertHostId != null) { + sc.setParameters("convertHostId", convertHostId); + } + return 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 @@ + 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 1) { @@ -210,17 +215,18 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper 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 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 destinationDisks = moveTemporaryDisksToDestination(temporaryDisks, - destinationStoragePools, storagePoolMgr); - - cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid); + List 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 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 tasks; + if (listAll) { + tasks = importVMTaskDao.listAll(); + } else { + tasks = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId, showCompleted); + } + + List responses = new ArrayList<>(); + for (ImportVMTaskVO task : tasks) { + responses.add(createImportVMTaskResponse(task)); + } + ListResponse 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 importUnmanagedInstancesSupportedHypervisors = Arrays.asList(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.KVM); + private static final List forceConvertToPoolAllowedTypes = + Arrays.asList(Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.Filesystem, + Storage.StoragePoolType.SharedMountPoint); + + ConfigKey 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 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 pools = primaryDataStoreDao.listPoolsByCluster(cluster.getId()); + Set 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 allowedParams = Arrays.asList(allowedParamsStr.split(",")); + List 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 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 convertStoragePools = findInstanceConversionStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap); + destinationCluster, convertHost, importHost, convertStoragePoolId, forceConvertToPool); + List 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 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 nicNetworkMap, @@ -1953,20 +2061,25 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { String sourceVM, UnmanagedInstanceTO sourceVMwareInstance, HostVO convertHost, HostVO importHost, List convertStoragePools, ServiceOfferingVO serviceOffering, Map 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 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 convertStoragePools, ServiceOfferingVO serviceOffering, Map 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 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 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 findInstanceConversionStoragePoolsInCluster( + private List findInstanceConversionDestinationStoragePoolsInCluster( Cluster destinationCluster, ServiceOfferingVO serviceOffering, - Map dataDiskOfferingMap - ) { - List 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 dataDiskOfferingMap, + DataStoreTO temporaryConvertLocation, boolean forceConvertToPool) { + List poolsList; + if (!forceConvertToPool) { + Set 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 pools, String tags) { @@ -2128,39 +2249,63 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { throw new CloudRuntimeException(msg); } - 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(); + 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, 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 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 @@ + + 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 configKeyMockParamsAllowed; + @Mock + private ConfigKey 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 @@ + + + + - + + + + {{ $t('message.select.extra.parameters.for.instance.conversion') }} + + +