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 <abhishek.mrt22@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Suresh Kumar Anaparti <sureshkumar.anaparti@gmail.com>
This commit is contained in:
Nicolas Vazquez 2025-10-10 20:00:29 -03:00 committed by GitHub
parent b99a03092f
commit b106d6e190
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1950 additions and 281 deletions

View File

@ -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. # If set to true, creates VMs as full clones of their templates on KVM hypervisor. Creates as linked clones otherwise.
# create.full.clone=false # 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=

View File

@ -794,6 +794,20 @@ public class AgentProperties{
*/ */
public static final Property<Boolean> VIRTV2V_VERBOSE_ENABLED = new Property<>("virtv2v.verbose.enabled", false); public static final Property<Boolean> VIRTV2V_VERBOSE_ENABLED = new Property<>("virtv2v.verbose.enabled", false);
/**
* Set env TMPDIR var for virt-v2v Instance Conversion from VMware to KVM
* Data type: String.<br>
* Default value: <code>null</code>
*/
public static final Property<String> CONVERT_ENV_TMPDIR = new Property<>("convert.instance.env.tmpdir", null, String.class);
/**
* Set env VIRT_V2V_TMPDIR var for virt-v2v Instance Conversion from VMware to KVM
* Data type: String.<br>
* Default value: <code>null</code>
*/
public static final Property<String> CONVERT_ENV_VIRTV2V_TMPDIR = new Property<>("convert.instance.env.virtv2v.tmpdir", null, String.class);
/** /**
* BGP controll CIDR * BGP controll CIDR
* Data type: String.<br> * Data type: String.<br>

View File

@ -225,6 +225,7 @@ public class ApiConstants {
public static final String EVENT_TYPE = "eventtype"; public static final String EVENT_TYPE = "eventtype";
public static final String EXPIRES = "expires"; public static final String EXPIRES = "expires";
public static final String EXTRA_CONFIG = "extraconfig"; 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 = "extradhcpoption";
public static final String EXTRA_DHCP_OPTION_NAME = "extradhcpoptionname"; public static final String EXTRA_DHCP_OPTION_NAME = "extradhcpoptionname";
public static final String EXTRA_DHCP_OPTION_CODE = "extradhcpoptioncode"; 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 FIRSTNAME = "firstname";
public static final String FORCED = "forced"; public static final String FORCED = "forced";
public static final String FORCED_DESTROY_LOCAL_STORAGE = "forcedestroylocalstorage"; 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_DELETE_HOST = "forcedeletehost";
public static final String FORCE_MS_TO_IMPORT_VM_FILES = "forcemstoimportvmfiles"; public static final String FORCE_MS_TO_IMPORT_VM_FILES = "forcemstoimportvmfiles";
public static final String FORCE_UPDATE_OS_TYPE = "forceupdateostype"; 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_CAPACITIES = "showcapacities";
public static final String SHOW_REMOVED = "showremoved"; public static final String SHOW_REMOVED = "showremoved";
public static final String SHOW_RESOURCE_ICON = "showicon"; 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_INACTIVE = "showinactive";
public static final String SHOW_UNIQUE = "showunique"; public static final String SHOW_UNIQUE = "showunique";
public static final String SIGNATURE = "signature"; public static final String SIGNATURE = "signature";

View File

@ -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.") 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; 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 /////////////////////// /////////////////// Accessors ///////////////////////
///////////////////////////////////////////////////// /////////////////////////////////////////////////////
@ -248,6 +260,14 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd {
return EventTypes.EVENT_VM_IMPORT; return EventTypes.EVENT_VM_IMPORT;
} }
public String getExtraParams() {
return extraParams;
}
public boolean getForceConvertToPool() {
return BooleanUtils.toBooleanDefaultIfNull(forceConvertToPool, false);
}
@Override @Override
public String getEventDescription() { public String getEventDescription() {
String vmName = getName(); String vmName = getName();

View File

@ -0,0 +1,123 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.api.command.admin.vm;
import com.cloud.exception.ConcurrentOperationException;
import com.cloud.exception.InsufficientCapacityException;
import com.cloud.exception.NetworkRuleConflictException;
import com.cloud.exception.ResourceAllocationException;
import com.cloud.exception.ResourceUnavailableException;
import com.cloud.user.Account;
import org.apache.cloudstack.acl.RoleType;
import org.apache.cloudstack.api.APICommand;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.BaseListCmd;
import org.apache.cloudstack.api.Parameter;
import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException;
import org.apache.cloudstack.api.response.AccountResponse;
import org.apache.cloudstack.api.response.HostResponse;
import org.apache.cloudstack.api.response.ImportVMTaskResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.cloudstack.api.response.ZoneResponse;
import org.apache.cloudstack.context.CallContext;
import org.apache.cloudstack.vm.ImportVmTasksManager;
import javax.inject.Inject;
@APICommand(name = "listImportVmTasks",
description = "List running import virtual machine tasks from a unmanaged hosts into CloudStack",
responseObject = ImportVMTaskResponse.class,
responseView = ResponseObject.ResponseView.Full,
requestHasSensitiveInfo = false,
authorized = {RoleType.Admin},
since = "4.22")
public class ListImportVMTasksCmd extends BaseListCmd {
@Inject
public ImportVmTasksManager importVmTasksManager;
@Parameter(name = ApiConstants.ZONE_ID,
type = CommandType.UUID,
entityType = ZoneResponse.class,
required = true,
description = "the zone ID")
private Long zoneId;
@Parameter(name = ApiConstants.ACCOUNT_ID,
type = CommandType.UUID,
entityType = AccountResponse.class,
description = "the ID of the Account")
private Long accountId;
@Parameter(name = ApiConstants.VCENTER,
type = CommandType.STRING,
description = "The name/ip of vCenter. Make sure it is IP address or full qualified domain name for host running vCenter server.")
private String vcenter;
@Parameter(name = ApiConstants.CONVERT_INSTANCE_HOST_ID,
type = CommandType.UUID,
entityType = HostResponse.class,
description = "Conversion host of the importing task")
private Long convertHostId;
@Parameter(name = ApiConstants.LIST_ALL, type = CommandType.BOOLEAN, description = "Whether to list all import tasks.")
private boolean listAll = false;
@Parameter(name = ApiConstants.SHOW_COMPLETED, type = CommandType.BOOLEAN, description = "Whether to list completed tasks.")
private boolean showCompleted = false;
public Long getZoneId() {
return zoneId;
}
public Long getAccountId() {
return accountId;
}
public String getVcenter() {
return vcenter;
}
public Long getConvertHostId() {
return convertHostId;
}
public boolean isListAll() {
return listAll;
}
public boolean isShowCompleted() {
return showCompleted;
}
@Override
public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException {
ListResponse<ImportVMTaskResponse> response = importVmTasksManager.listImportVMTasks(this);
response.setResponseName(getCommandName());
setResponseObject(response);
}
@Override
public long getEntityOwnerId() {
Account account = CallContext.current().getCallingAccount();
if (account != null) {
return account.getId();
}
return Account.ACCOUNT_ID_SYSTEM;
}
}

View File

@ -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;
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,38 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.vm;
import com.cloud.dc.DataCenter;
import com.cloud.host.Host;
import com.cloud.user.Account;
import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd;
import org.apache.cloudstack.api.response.ImportVMTaskResponse;
import org.apache.cloudstack.api.response.ListResponse;
public interface ImportVmTasksManager {
ListResponse<ImportVMTaskResponse> listImportVMTasks(ListImportVMTasksCmd cmd);
ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName,
String vcenter, String datacenterName, String sourceVMName,
Host convertHost, Host importHost);
void updateImportVMTaskStep(ImportVmTask importVMTaskVO, DataCenter zone, Account owner, Host convertHost,
Host importHost, Long vmId, ImportVmTask.Step step);
boolean removeImportVMTask(long taskId);
}

View File

@ -23,30 +23,37 @@ import com.cloud.hypervisor.Hypervisor;
public class ConvertInstanceCommand extends Command { public class ConvertInstanceCommand extends Command {
private RemoteInstanceTO sourceInstance; private RemoteInstanceTO sourceInstance;
private String originalVMName;
private Hypervisor.HypervisorType destinationHypervisorType; private Hypervisor.HypervisorType destinationHypervisorType;
private DataStoreTO conversionTemporaryLocation; private DataStoreTO conversionTemporaryLocation;
private String templateDirOnConversionLocation; private String templateDirOnConversionLocation;
private boolean checkConversionSupport; private boolean checkConversionSupport;
private boolean exportOvfToConversionLocation; private boolean exportOvfToConversionLocation;
private int threadsCountToExportOvf = 0; private int threadsCountToExportOvf = 0;
private String extraParams;
public ConvertInstanceCommand() { public ConvertInstanceCommand() {
} }
public ConvertInstanceCommand(RemoteInstanceTO sourceInstance, Hypervisor.HypervisorType destinationHypervisorType, DataStoreTO conversionTemporaryLocation, 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.sourceInstance = sourceInstance;
this.destinationHypervisorType = destinationHypervisorType; this.destinationHypervisorType = destinationHypervisorType;
this.conversionTemporaryLocation = conversionTemporaryLocation; this.conversionTemporaryLocation = conversionTemporaryLocation;
this.templateDirOnConversionLocation = templateDirOnConversionLocation; this.templateDirOnConversionLocation = templateDirOnConversionLocation;
this.checkConversionSupport = checkConversionSupport; this.checkConversionSupport = checkConversionSupport;
this.exportOvfToConversionLocation = exportOvfToConversionLocation; this.exportOvfToConversionLocation = exportOvfToConversionLocation;
this.originalVMName = sourceVMName;
} }
public RemoteInstanceTO getSourceInstance() { public RemoteInstanceTO getSourceInstance() {
return sourceInstance; return sourceInstance;
} }
public String getOriginalVMName() {
return originalVMName;
}
public Hypervisor.HypervisorType getDestinationHypervisorType() { public Hypervisor.HypervisorType getDestinationHypervisorType() {
return destinationHypervisorType; return destinationHypervisorType;
} }
@ -75,6 +82,14 @@ public class ConvertInstanceCommand extends Command {
this.threadsCountToExportOvf = threadsCountToExportOvf; this.threadsCountToExportOvf = threadsCountToExportOvf;
} }
public String getExtraParams() {
return extraParams;
}
public void setExtraParams(String extraParams) {
this.extraParams = extraParams;
}
@Override @Override
public boolean executeInSequence() { public boolean executeInSequence() {
return false; return false;

View File

@ -27,17 +27,20 @@ public class ImportConvertedInstanceCommand extends Command {
private List<String> destinationStoragePools; private List<String> destinationStoragePools;
private DataStoreTO conversionTemporaryLocation; private DataStoreTO conversionTemporaryLocation;
private String temporaryConvertUuid; private String temporaryConvertUuid;
private boolean forceConvertToPool;
public ImportConvertedInstanceCommand() { public ImportConvertedInstanceCommand() {
} }
public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance, public ImportConvertedInstanceCommand(RemoteInstanceTO sourceInstance,
List<String> destinationStoragePools, List<String> destinationStoragePools,
DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid) { DataStoreTO conversionTemporaryLocation, String temporaryConvertUuid,
boolean forceConvertToPool) {
this.sourceInstance = sourceInstance; this.sourceInstance = sourceInstance;
this.destinationStoragePools = destinationStoragePools; this.destinationStoragePools = destinationStoragePools;
this.conversionTemporaryLocation = conversionTemporaryLocation; this.conversionTemporaryLocation = conversionTemporaryLocation;
this.temporaryConvertUuid = temporaryConvertUuid; this.temporaryConvertUuid = temporaryConvertUuid;
this.forceConvertToPool = forceConvertToPool;
} }
public RemoteInstanceTO getSourceInstance() { public RemoteInstanceTO getSourceInstance() {
@ -56,6 +59,10 @@ public class ImportConvertedInstanceCommand extends Command {
return temporaryConvertUuid; return temporaryConvertUuid;
} }
public boolean isForceConvertToPool() {
return forceConvertToPool;
}
@Override @Override
public boolean executeInSequence() { public boolean executeInSequence() {
return false; return false;

View File

@ -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;
}
}

View File

@ -0,0 +1,29 @@
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
package com.cloud.vm.dao;
import com.cloud.utils.db.GenericDao;
import com.cloud.vm.ImportVMTaskVO;
import java.util.List;
public interface ImportVMTaskDao extends GenericDao<ImportVMTaskVO, Long> {
List<ImportVMTaskVO> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted);
}

View File

@ -0,0 +1,65 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.vm.dao;
import com.cloud.utils.db.GenericDaoBase;
import com.cloud.utils.db.SearchBuilder;
import com.cloud.utils.db.SearchCriteria;
import com.cloud.vm.ImportVMTaskVO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
@Component
public class ImportVMTaskDaoImpl extends GenericDaoBase<ImportVMTaskVO, Long> implements ImportVMTaskDao {
private SearchBuilder<ImportVMTaskVO> AllFieldsSearch;
public ImportVMTaskDaoImpl() {
}
@PostConstruct
void init() {
AllFieldsSearch = createSearchBuilder();
AllFieldsSearch.and("zoneId", AllFieldsSearch.entity().getZoneId(), SearchCriteria.Op.EQ);
AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), SearchCriteria.Op.EQ);
AllFieldsSearch.and("vcenter", AllFieldsSearch.entity().getVcenter(), SearchCriteria.Op.EQ);
AllFieldsSearch.and("convertHostId", AllFieldsSearch.entity().getConvertHostId(), SearchCriteria.Op.EQ);
AllFieldsSearch.done();
}
@Override
public List<ImportVMTaskVO> listImportVMTasks(Long zoneId, Long accountId, String vcenter, Long convertHostId, boolean showCompleted) {
SearchCriteria<ImportVMTaskVO> sc = AllFieldsSearch.create();
if (zoneId != null) {
sc.setParameters("zoneId", zoneId);
}
if (accountId != null) {
sc.setParameters("accountId", accountId);
}
if (StringUtils.isNotBlank(vcenter)) {
sc.setParameters("vcenter", vcenter);
}
if (convertHostId != null) {
sc.setParameters("convertHostId", convertHostId);
}
return showCompleted ? listIncludingRemovedBy(sc) : listBy(sc);
}
}

View File

@ -309,4 +309,5 @@
<bean id="gpuCardDaoImpl" class="com.cloud.gpu.dao.GpuCardDaoImpl" /> <bean id="gpuCardDaoImpl" class="com.cloud.gpu.dao.GpuCardDaoImpl" />
<bean id="gpuDeviceDaoImpl" class="com.cloud.gpu.dao.GpuDeviceDaoImpl" /> <bean id="gpuDeviceDaoImpl" class="com.cloud.gpu.dao.GpuDeviceDaoImpl" />
<bean id="vgpuProfileDaoImpl" class="com.cloud.gpu.dao.VgpuProfileDaoImpl" /> <bean id="vgpuProfileDaoImpl" class="com.cloud.gpu.dao.VgpuProfileDaoImpl" />
<bean id="importVMTaskDaoImpl" class="com.cloud.vm.dao.ImportVMTaskDaoImpl" />
</beans> </beans>

View File

@ -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 '%password%';
UPDATE `cloud`.`storage_pool_details` SET display = 0 WHERE name LIKE '%token%'; 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_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); CALL `cloud`.`INSERT_EXTENSION_DETAIL_IF_NOT_EXISTS`('MaaS', 'orchestratorrequirespreparevm', 'true', 0);

View File

@ -882,6 +882,7 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
protected StorageSubsystemCommandHandler storageHandler; protected StorageSubsystemCommandHandler storageHandler;
private boolean convertInstanceVerboseMode = false; private boolean convertInstanceVerboseMode = false;
private String[] convertInstanceEnv = null;
protected boolean dpdkSupport = false; protected boolean dpdkSupport = false;
protected String dpdkOvsPath; protected String dpdkOvsPath;
protected String directDownloadTemporaryDownloadPath; protected String directDownloadTemporaryDownloadPath;
@ -946,6 +947,10 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
return convertInstanceVerboseMode; return convertInstanceVerboseMode;
} }
public String[] getConvertInstanceEnv() {
return convertInstanceEnv;
}
/** /**
* Defines resource's public and private network interface according to what is configured in agent.properties. * 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)); 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"); pool = (String)params.get("pool");
if (pool == null) { if (pool == null) {
pool = "/root"; pool = "/root";
@ -1422,6 +1432,22 @@ public class LibvirtComputingResource extends ServerResourceBase implements Serv
return true; 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. * Parses a string containing whitespace-separated CPU feature names and converts it into a list.
* *

View File

@ -20,6 +20,7 @@ package com.cloud.hypervisor.kvm.resource.wrapper;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -57,11 +58,13 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
Hypervisor.HypervisorType destinationHypervisorType = cmd.getDestinationHypervisorType(); Hypervisor.HypervisorType destinationHypervisorType = cmd.getDestinationHypervisorType();
DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation(); DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation();
long timeout = (long) cmd.getWait() * 1000; long timeout = (long) cmd.getWait() * 1000;
String extraParams = cmd.getExtraParams();
String originalVMName = cmd.getOriginalVMName(); // For logging purposes, as the sourceInstance may have been cloned
if (cmd.getCheckConversionSupport() && !serverResource.hostSupportsInstanceConversion()) { if (cmd.getCheckConversionSupport() && !serverResource.hostSupportsInstanceConversion()) {
String msg = String.format("Cannot convert the instance %s from VMware as the virt-v2v binary is not found. " + String msg = String.format("Cannot convert the instance %s from VMware as the virt-v2v binary is not found. " +
"Please install virt-v2v%s on the host before attempting the instance conversion.", sourceInstanceName, serverResource.isUbuntuOrDebianHost()? ", nbdkit" : ""); "Please install virt-v2v%s on the host before attempting the instance conversion.", sourceInstanceName, serverResource.isUbuntuOrDebianHost()? ", nbdkit" : "");
logger.info(msg); logger.info(String.format("(%s) %s", originalVMName, msg));
return new Answer(cmd, false, msg); return new Answer(cmd, false, msg);
} }
@ -69,25 +72,25 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
String err = destinationHypervisorType != Hypervisor.HypervisorType.KVM ? String err = destinationHypervisorType != Hypervisor.HypervisorType.KVM ?
String.format("The destination hypervisor type is %s, KVM was expected, cannot handle it", destinationHypervisorType) : String.format("The destination hypervisor type is %s, KVM was expected, cannot handle it", destinationHypervisorType) :
String.format("The source hypervisor type %s is not supported for KVM conversion", sourceHypervisorType); String.format("The source hypervisor type %s is not supported for KVM conversion", sourceHypervisorType);
logger.error(err); logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err); return new Answer(cmd, false, err);
} }
final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr); KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr);
logger.info(String.format("Attempting to convert the instance %s from %s to KVM", logger.info(String.format("(%s) Attempting to convert the instance %s from %s to KVM",
sourceInstanceName, sourceHypervisorType)); originalVMName, sourceInstanceName, sourceHypervisorType));
final String temporaryConvertPath = temporaryStoragePool.getLocalPath(); final String temporaryConvertPath = temporaryStoragePool.getLocalPath();
String ovfTemplateDirOnConversionLocation; String ovfTemplateDirOnConversionLocation;
String sourceOVFDirPath; String sourceOVFDirPath;
boolean ovfExported = false; boolean ovfExported = false;
if (cmd.getExportOvfToConversionLocation()) { if (cmd.getExportOvfToConversionLocation()) {
String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance); String exportInstanceOVAUrl = getExportInstanceOVAUrl(sourceInstance, originalVMName);
if (StringUtils.isBlank(exportInstanceOVAUrl)) { if (StringUtils.isBlank(exportInstanceOVAUrl)) {
String err = String.format("Couldn't export OVA for the VM %s, due to empty url", sourceInstanceName); String err = String.format("Couldn't export OVA for the VM %s, due to empty url", sourceInstanceName);
logger.error(err); logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err); return new Answer(cmd, false, err);
} }
@ -98,10 +101,10 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
ovfTemplateDirOnConversionLocation = UUID.randomUUID().toString(); ovfTemplateDirOnConversionLocation = UUID.randomUUID().toString();
temporaryStoragePool.createFolder(ovfTemplateDirOnConversionLocation); temporaryStoragePool.createFolder(ovfTemplateDirOnConversionLocation);
sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, timeout); ovfExported = exportOVAFromVMOnVcenter(exportInstanceOVAUrl, sourceOVFDirPath, noOfThreads, originalVMName, timeout);
if (!ovfExported) { if (!ovfExported) {
String err = String.format("Export OVA for the VM %s failed", sourceInstanceName); String err = String.format("Export OVA for the VM %s failed", sourceInstanceName);
logger.error(err); logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err); return new Answer(cmd, false, err);
} }
sourceOVFDirPath = String.format("%s%s/", sourceOVFDirPath, sourceInstanceName); sourceOVFDirPath = String.format("%s%s/", sourceOVFDirPath, sourceInstanceName);
@ -110,38 +113,40 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation); sourceOVFDirPath = String.format("%s/%s/", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
} }
logger.info(String.format("Attempting to convert the OVF %s of the instance %s from %s to KVM", ovfTemplateDirOnConversionLocation, sourceInstanceName, sourceHypervisorType)); logger.info(String.format("(%s) Attempting to convert the OVF %s of the instance %s from %s to KVM",
originalVMName, ovfTemplateDirOnConversionLocation, sourceInstanceName, sourceHypervisorType));
final String temporaryConvertUuid = UUID.randomUUID().toString(); final String temporaryConvertUuid = UUID.randomUUID().toString();
boolean verboseModeEnabled = serverResource.isConvertInstanceVerboseModeEnabled(); boolean verboseModeEnabled = serverResource.isConvertInstanceVerboseModeEnabled();
boolean cleanupSecondaryStorage = false; boolean cleanupSecondaryStorage = false;
try { try {
boolean result = performInstanceConversion(sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid, boolean result = performInstanceConversion(originalVMName, sourceOVFDirPath, temporaryConvertPath, temporaryConvertUuid,
timeout, verboseModeEnabled); timeout, verboseModeEnabled, extraParams, serverResource);
if (!result) { if (!result) {
String err = String.format( String err = String.format(
"The virt-v2v conversion for the OVF %s failed. Please check the agent logs " + "The virt-v2v conversion for the OVF %s failed. Please check the agent logs " +
"for the virt-v2v output. Please try on a different kvm host which " + "for the virt-v2v output. Please try on a different kvm host which " +
"has a different virt-v2v version.", "has a different virt-v2v version.",
ovfTemplateDirOnConversionLocation); ovfTemplateDirOnConversionLocation);
logger.error(err); logger.error(String.format("(%s) %s", originalVMName, err));
return new Answer(cmd, false, err); return new Answer(cmd, false, err);
} }
return new ConvertInstanceAnswer(cmd, temporaryConvertUuid); return new ConvertInstanceAnswer(cmd, temporaryConvertUuid);
} catch (Exception e) { } catch (Exception e) {
String error = String.format("Error converting instance %s from %s, due to: %s", String error = String.format("Error converting instance %s from %s, due to: %s",
sourceInstanceName, sourceHypervisorType, e.getMessage()); sourceInstanceName, sourceHypervisorType, e.getMessage());
logger.error(error, e); logger.error(String.format("(%s) %s", originalVMName, error), e);
cleanupSecondaryStorage = true; cleanupSecondaryStorage = true;
return new Answer(cmd, false, error); return new Answer(cmd, false, error);
} finally { } finally {
if (ovfExported && StringUtils.isNotBlank(ovfTemplateDirOnConversionLocation)) { if (ovfExported && StringUtils.isNotBlank(ovfTemplateDirOnConversionLocation)) {
String sourceOVFDir = String.format("%s/%s", temporaryConvertPath, ovfTemplateDirOnConversionLocation); String sourceOVFDir = String.format("%s/%s", temporaryConvertPath, ovfTemplateDirOnConversionLocation);
logger.debug("Cleaning up exported OVA at dir " + sourceOVFDir); logger.debug("({}) Cleaning up exported OVA at dir: {}", originalVMName, sourceOVFDir);
FileUtil.deletePath(sourceOVFDir); FileUtil.deletePath(sourceOVFDir);
} }
if (cleanupSecondaryStorage && conversionTemporaryLocation instanceof NfsTO) { if (cleanupSecondaryStorage && conversionTemporaryLocation instanceof NfsTO) {
logger.debug("Cleaning up secondary storage temporary location"); logger.debug("({}) Cleaning up secondary storage temporary location", originalVMName);
storagePoolMgr.deleteStoragePool(temporaryStoragePool.getType(), temporaryStoragePool.getUuid()); storagePoolMgr.deleteStoragePool(temporaryStoragePool.getType(), temporaryStoragePool.getUuid());
} }
} }
@ -163,15 +168,15 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
supportedInstanceConvertSourceHypervisors.contains(sourceHypervisorType); supportedInstanceConvertSourceHypervisors.contains(sourceHypervisorType);
} }
private String getExportInstanceOVAUrl(RemoteInstanceTO sourceInstance) { private String getExportInstanceOVAUrl(RemoteInstanceTO sourceInstance, String originalVMName) {
String url = null; String url = null;
if (sourceInstance.getHypervisorType() == Hypervisor.HypervisorType.VMware) { if (sourceInstance.getHypervisorType() == Hypervisor.HypervisorType.VMware) {
url = getExportOVAUrlFromRemoteInstance(sourceInstance); url = getExportOVAUrlFromRemoteInstance(sourceInstance, originalVMName);
} }
return url; return url;
} }
private String getExportOVAUrlFromRemoteInstance(RemoteInstanceTO vmwareInstance) { private String getExportOVAUrlFromRemoteInstance(RemoteInstanceTO vmwareInstance, String originalVMName) {
String vcenter = vmwareInstance.getVcenterHost(); String vcenter = vmwareInstance.getVcenterHost();
String username = vmwareInstance.getVcenterUsername(); String username = vmwareInstance.getVcenterUsername();
String password = vmwareInstance.getVcenterPassword(); String password = vmwareInstance.getVcenterPassword();
@ -182,7 +187,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
String encodedUsername = encodeUsername(username); String encodedUsername = encodeUsername(username);
String encodedPassword = encodeUsername(password); String encodedPassword = encodeUsername(password);
if (StringUtils.isNotBlank(path)) { if (StringUtils.isNotBlank(path)) {
logger.info("VM path: {}", path); logger.info("({}) VM path: {}", originalVMName, path);
return String.format("vi://%s:%s@%s/%s/%s/%s", return String.format("vi://%s:%s@%s/%s/%s/%s",
encodedUsername, encodedPassword, vcenter, datacenter, path, vm); encodedUsername, encodedPassword, vcenter, datacenter, path, vm);
} }
@ -201,7 +206,7 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
private boolean exportOVAFromVMOnVcenter(String vmExportUrl, private boolean exportOVAFromVMOnVcenter(String vmExportUrl,
String targetOvfDir, String targetOvfDir,
int noOfThreads, int noOfThreads,
long timeout) { String originalVMName, long timeout) {
Script script = new Script("ovftool", timeout, logger); Script script = new Script("ovftool", timeout, logger);
script.add("--noSSLVerify"); script.add("--noSSLVerify");
if (noOfThreads > 1) { if (noOfThreads > 1) {
@ -210,17 +215,18 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
script.add(vmExportUrl); script.add(vmExportUrl);
script.add(targetOvfDir); script.add(targetOvfDir);
String logPrefix = "export ovf"; String logPrefix = String.format("(%s) export ovf", originalVMName);
OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix);
script.execute(outputLogger); script.execute(outputLogger);
int exitValue = script.getExitValue(); int exitValue = script.getExitValue();
return exitValue == 0; return exitValue == 0;
} }
protected boolean performInstanceConversion(String sourceOVFDirPath, protected boolean performInstanceConversion(String originalVMName, String sourceOVFDirPath,
String temporaryConvertFolder, String temporaryConvertFolder,
String temporaryConvertUuid, String temporaryConvertUuid,
long timeout, boolean verboseModeEnabled) { long timeout, boolean verboseModeEnabled, String extraParams,
LibvirtComputingResource serverResource) {
Script script = new Script("virt-v2v", timeout, logger); Script script = new Script("virt-v2v", timeout, logger);
script.add("--root", "first"); script.add("--root", "first");
script.add("-i", "ova"); script.add("-i", "ova");
@ -232,14 +238,33 @@ public class LibvirtConvertInstanceCommandWrapper extends CommandWrapper<Convert
if (verboseModeEnabled) { if (verboseModeEnabled) {
script.add("-v"); script.add("-v");
} }
if (StringUtils.isNotBlank(extraParams)) {
addExtraParamsToScript(extraParams, script);
}
String logPrefix = String.format("virt-v2v ovf source: %s progress", sourceOVFDirPath); String logPrefix = String.format("(%s) virt-v2v ovf source: %s progress", originalVMName, sourceOVFDirPath);
OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix); OutputInterpreter.LineByLineOutputLogger outputLogger = new OutputInterpreter.LineByLineOutputLogger(logger, logPrefix);
script.execute(outputLogger); script.execute(outputLogger);
int exitValue = script.getExitValue(); int exitValue = script.getExitValue();
return exitValue == 0; return exitValue == 0;
} }
protected void addExtraParamsToScript(String extraParams, Script script) {
List<String> separatedArgs = Arrays.asList(extraParams.split(" "));
int i = 0;
while (i < separatedArgs.size()) {
String current = separatedArgs.get(i);
String next = (i + 1) < separatedArgs.size() ? separatedArgs.get(i + 1) : null;
if (next == null || next.startsWith("-")) {
script.add(current);
i = i + 1;
} else {
script.add(current, next);
i = i + 2;
}
}
}
protected String encodeUsername(String username) { protected String encodeUsername(String username) {
return URLEncoder.encode(username, Charset.defaultCharset()); return URLEncoder.encode(username, Charset.defaultCharset());
} }

View File

@ -67,6 +67,7 @@ public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper
List<String> destinationStoragePools = cmd.getDestinationStoragePools(); List<String> destinationStoragePools = cmd.getDestinationStoragePools();
DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation(); DataStoreTO conversionTemporaryLocation = cmd.getConversionTemporaryLocation();
final String temporaryConvertUuid = cmd.getTemporaryConvertUuid(); final String temporaryConvertUuid = cmd.getTemporaryConvertUuid();
final boolean forceConvertToPool = cmd.isForceConvertToPool();
final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr(); final KVMStoragePoolManager storagePoolMgr = serverResource.getStoragePoolMgr();
KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr); KVMStoragePool temporaryStoragePool = getTemporaryStoragePool(conversionTemporaryLocation, storagePoolMgr);
@ -80,13 +81,18 @@ public class LibvirtImportConvertedInstanceCommandWrapper extends CommandWrapper
getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid) : getTemporaryDisksWithPrefixFromTemporaryPool(temporaryStoragePool, temporaryConvertPath, temporaryConvertUuid) :
getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath); getTemporaryDisksFromParsedXml(temporaryStoragePool, xmlParser, convertedBasePath);
List<KVMPhysicalDisk> destinationDisks = moveTemporaryDisksToDestination(temporaryDisks, List<KVMPhysicalDisk> disks = null;
if (forceConvertToPool) {
// Force flag to use the conversion path, no need to move disks
disks = temporaryDisks;
} else {
disks = moveTemporaryDisksToDestination(temporaryDisks,
destinationStoragePools, storagePoolMgr); destinationStoragePools, storagePoolMgr);
cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid); cleanupDisksAndDomainFromTemporaryLocation(temporaryDisks, temporaryStoragePool, temporaryConvertUuid);
}
UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid, UnmanagedInstanceTO convertedInstanceTO = getConvertedUnmanagedInstance(temporaryConvertUuid,
destinationDisks, xmlParser); disks, xmlParser);
return new ImportConvertedInstanceAnswer(cmd, convertedInstanceTO); return new ImportConvertedInstanceAnswer(cmd, convertedInstanceTO);
} catch (Exception e) { } catch (Exception e) {
String error = String.format("Error converting instance %s from %s, due to: %s", String error = String.format("Error converting instance %s from %s, due to: %s",

View File

@ -128,6 +128,7 @@ public class LibvirtConvertInstanceCommandWrapperTest {
Mockito.when(cmd.getWait()).thenReturn(14400); Mockito.when(cmd.getWait()).thenReturn(14400);
Mockito.when(cmd.getConversionTemporaryLocation()).thenReturn(secondaryDataStore); Mockito.when(cmd.getConversionTemporaryLocation()).thenReturn(secondaryDataStore);
Mockito.when(cmd.getCheckConversionSupport()).thenReturn(checkConversionSupport); Mockito.when(cmd.getCheckConversionSupport()).thenReturn(checkConversionSupport);
Mockito.when(cmd.getOriginalVMName()).thenReturn(vmName);
return cmd; return cmd;
} }
@ -166,8 +167,26 @@ public class LibvirtConvertInstanceCommandWrapperTest {
Answer answer = convertInstanceCommandWrapper.execute(cmd, libvirtComputingResourceMock); Answer answer = convertInstanceCommandWrapper.execute(cmd, libvirtComputingResourceMock);
Assert.assertFalse(answer.getResult()); Assert.assertFalse(answer.getResult());
Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(), Mockito.verify(convertInstanceCommandWrapper).performInstanceConversion(Mockito.anyString(), Mockito.anyString(),
Mockito.anyString(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean()); 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");
}
} }

View File

@ -0,0 +1,225 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package org.apache.cloudstack.vm;
import com.cloud.dc.DataCenter;
import com.cloud.dc.DataCenterVO;
import com.cloud.dc.dao.DataCenterDao;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.dao.HostDao;
import com.cloud.user.Account;
import com.cloud.user.AccountService;
import com.cloud.utils.DateUtil;
import com.cloud.vm.ImportVMTaskVO;
import com.cloud.vm.UserVmVO;
import com.cloud.vm.dao.ImportVMTaskDao;
import com.cloud.vm.dao.UserVmDao;
import org.apache.cloudstack.api.command.admin.vm.ListImportVMTasksCmd;
import org.apache.cloudstack.api.response.ImportVMTaskResponse;
import org.apache.cloudstack.api.response.ListResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.inject.Inject;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
import static org.apache.cloudstack.vm.ImportVmTask.Step.CloningInstance;
import static org.apache.cloudstack.vm.ImportVmTask.Step.Completed;
import static org.apache.cloudstack.vm.ImportVmTask.Step.ConvertingInstance;
import static org.apache.cloudstack.vm.ImportVmTask.Step.Importing;
import static org.apache.cloudstack.vm.ImportVmTask.Step.Prepare;
public class ImportVmTasksManagerImpl implements ImportVmTasksManager {
protected Logger logger = LogManager.getLogger(ImportVmTasksManagerImpl.class);
@Inject
private ImportVMTaskDao importVMTaskDao;
@Inject
private DataCenterDao dataCenterDao;
@Inject
private AccountService accountService;
@Inject
private HostDao hostDao;
@Inject
private UserVmDao userVmDao;
public ImportVmTasksManagerImpl() {
}
@Override
public ListResponse<ImportVMTaskResponse> listImportVMTasks(ListImportVMTasksCmd cmd) {
Long zoneId = cmd.getZoneId();
Long accountId = cmd.getAccountId();
String vcenter = cmd.getVcenter();
Long convertHostId = cmd.getConvertHostId();
boolean listAll = cmd.isListAll();
boolean showCompleted = cmd.isShowCompleted();
List<ImportVMTaskVO> tasks;
if (listAll) {
tasks = importVMTaskDao.listAll();
} else {
tasks = importVMTaskDao.listImportVMTasks(zoneId, accountId, vcenter, convertHostId, showCompleted);
}
List<ImportVMTaskResponse> responses = new ArrayList<>();
for (ImportVMTaskVO task : tasks) {
responses.add(createImportVMTaskResponse(task));
}
ListResponse<ImportVMTaskResponse> listResponses = new ListResponse<>();
listResponses.setResponses(responses, responses.size());
return listResponses;
}
@Override
public ImportVmTask createImportVMTaskRecord(DataCenter zone, Account owner, long userId, String displayName, String vcenter, String datacenterName, String sourceVMName, Host convertHost, Host importHost) {
logger.debug("Creating import VM task entry for VM: {} for account {} on zone {} " +
"from the vCenter: {} / datacenter: {} / source VM: {}",
sourceVMName, owner.getAccountName(), zone.getName(), displayName, vcenter, datacenterName);
ImportVMTaskVO importVMTaskVO = new ImportVMTaskVO(zone.getId(), owner.getAccountId(), userId, displayName,
vcenter, datacenterName, sourceVMName, convertHost.getId(), importHost.getId());
return importVMTaskDao.persist(importVMTaskVO);
}
private String getStepDescription(ImportVMTaskVO importVMTaskVO, Host convertHost, Host importHost,
ImportVMTaskVO.Step step, Date updatedDate) {
String sourceVMName = importVMTaskVO.getSourceVMName();
String vcenter = importVMTaskVO.getVcenter();
String datacenter = importVMTaskVO.getDatacenter();
StringBuilder stringBuilder = new StringBuilder();
if (Completed == step) {
stringBuilder.append("Completed at ").append(DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate));
} else {
stringBuilder.append(String.format("[%s] ", DateUtil.getDateDisplayString(TimeZone.getTimeZone("GMT"), updatedDate)));
if (CloningInstance == step) {
stringBuilder.append(String.format("Cloning source instance: %s on vCenter: %s / datacenter: %s", sourceVMName, vcenter, datacenter));
} else if (ConvertingInstance == step) {
stringBuilder.append(String.format("Converting the cloned VMware instance to a KVM instance on the host: %s", convertHost.getName()));
} else if (Importing == step) {
stringBuilder.append(String.format("Importing the converted KVM instance on the host: %s", importHost.getName()));
} else if (Prepare == step) {
stringBuilder.append("Preparing to convert Vmware instance");
}
}
return stringBuilder.toString();
}
@Override
public void updateImportVMTaskStep(ImportVmTask importVMTask, DataCenter zone, Account owner, Host convertHost,
Host importHost, Long vmId, ImportVmTask.Step step) {
ImportVMTaskVO importVMTaskVO = (ImportVMTaskVO) importVMTask;
logger.debug("Updating import VM task entry for VM: {} for account {} on zone {} " +
"from the vCenter: {} / datacenter: {} / source VM: {} to step: {}",
importVMTaskVO.getSourceVMName(), owner.getAccountName(), zone.getName(), importVMTaskVO.getDisplayName(),
importVMTaskVO.getVcenter(), importVMTaskVO.getDatacenter(), step);
Date updatedDate = DateUtil.now();
String description = getStepDescription(importVMTaskVO, convertHost, importHost, step, updatedDate);
importVMTaskVO.setStep(step);
importVMTaskVO.setDescription(description);
importVMTaskVO.setUpdated(updatedDate);
if (Completed == step) {
Duration duration = Duration.between(importVMTaskVO.getCreated().toInstant(), updatedDate.toInstant());
importVMTaskVO.setDuration(duration.toMillis());
importVMTaskVO.setVmId(vmId);
}
importVMTaskDao.update(importVMTaskVO.getId(), importVMTaskVO);
}
@Override
public boolean removeImportVMTask(long taskId) {
return importVMTaskDao.remove(taskId);
}
private ImportVMTaskResponse createImportVMTaskResponse(ImportVMTaskVO task) {
ImportVMTaskResponse response = new ImportVMTaskResponse();
DataCenterVO zone = dataCenterDao.findById(task.getZoneId());
if (zone != null) {
response.setZoneId(zone.getUuid());
response.setZoneName(zone.getName());
}
Account account = accountService.getAccount(task.getAccountId());
if (account != null) {
response.setAccountId(account.getUuid());
response.setAccountName(account.getAccountName());
}
response.setVcenter(task.getVcenter());
response.setDatacenterName(task.getDatacenter());
response.setSourceVMName(task.getSourceVMName());
response.setDisplayName(task.getDisplayName());
response.setStep(getStepDisplayField(task.getStep()));
response.setDescription(task.getDescription());
Date updated = task.getUpdated();
Date currentDate = new Date();
if (updated != null && Completed != task.getStep()) {
Duration stepDuration = Duration.between(updated.toInstant(), currentDate.toInstant());
response.setStepDuration(getDurationDisplay(stepDuration.toMillis()));
}
if (Completed == task.getStep()) {
response.setStepDuration(getDurationDisplay(task.getDuration()));
} else {
Duration totalDuration = Duration.between(task.getCreated().toInstant(), currentDate.toInstant());
response.setDuration(getDurationDisplay(totalDuration.toMillis()));
}
HostVO host = hostDao.findById(task.getConvertHostId());
if (host != null) {
response.setConvertInstanceHostId(host.getUuid());
response.setConvertInstanceHostName(host.getName());
}
if (task.getVmId() != null) {
UserVmVO userVm = userVmDao.findById(task.getVmId());
response.setVirtualMachineId(userVm.getUuid());
}
response.setCreated(task.getCreated());
response.setLastUpdated(task.getUpdated());
response.setObjectName("importvmtask");
return response;
}
protected String getStepDisplayField(ImportVMTaskVO.Step step) {
int totalSteps = ImportVMTaskVO.Step.values().length;
return String.format("[%s/%s] %s", step.ordinal() + 1, totalSteps, step.name());
}
protected static String getDurationDisplay(Long durationMs) {
if (durationMs == null) {
return null;
}
long hours = durationMs / (1000 * 60 * 60);
long minutes = (durationMs / (1000 * 60)) % 60;
long seconds = (durationMs / 1000) % 60;
StringBuilder result = new StringBuilder();
if (hours > 0) {
result.append(String.format("%s hs ", hours));
}
if (minutes > 0) {
result.append(String.format("%s min ", minutes));
}
if (seconds > 0) {
result.append(String.format("%s secs", seconds));
}
return result.toString();
}
}

View File

@ -122,6 +122,7 @@ import com.cloud.user.dao.UserDao;
import com.cloud.uservm.UserVm; import com.cloud.uservm.UserVm;
import com.cloud.utils.LogUtils; import com.cloud.utils.LogUtils;
import com.cloud.utils.Pair; import com.cloud.utils.Pair;
import com.cloud.utils.UuidUtils;
import com.cloud.utils.db.EntityManager; import com.cloud.utils.db.EntityManager;
import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.exception.CloudRuntimeException;
import com.cloud.utils.net.NetUtils; 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.ServerApiException;
import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; 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.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.ListUnmanagedInstancesCmd;
import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd;
import org.apache.cloudstack.api.command.admin.vm.UnmanageVMInstanceCmd; 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.cloudstack.utils.volume.VirtualMachineDiskInfo;
import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils; 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.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Logger;
@ -180,7 +184,9 @@ import org.apache.logging.log4j.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; 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.MAX_IOPS;
import static org.apache.cloudstack.api.ApiConstants.MIN_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 class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
public static final String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso"; public static final String VM_IMPORT_DEFAULT_TEMPLATE_NAME = "system-default-vm-import-dummy-template.iso";
@ -198,6 +208,28 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
private static final List<Hypervisor.HypervisorType> importUnmanagedInstancesSupportedHypervisors = private static final List<Hypervisor.HypervisorType> importUnmanagedInstancesSupportedHypervisors =
Arrays.asList(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.KVM); Arrays.asList(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.KVM);
private static final List<Storage.StoragePoolType> forceConvertToPoolAllowedTypes =
Arrays.asList(Storage.StoragePoolType.NetworkFilesystem, Storage.StoragePoolType.Filesystem,
Storage.StoragePoolType.SharedMountPoint);
ConfigKey<Boolean> ConvertVmwareInstanceToKvmExtraParamsAllowed = new ConfigKey<>(Boolean.class,
"convert.vmware.instance.to.kvm.extra.params.allowed",
"Advanced",
"false",
"Disabled by default. If enabled, allows extra parameters to be passed to the virt-v2v binary on KVM conversion hosts",
true,
ConfigKey.Scope.Global,
null);
ConfigKey<String> ConvertVmwareInstanceToKvmExtraParamsAllowedList = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED,
String.class,
"convert.vmware.instance.to.kvm.extra.params.allowed.list",
"",
"Comma separated list of allowed extra parameters to be passed to the virt-v2v binary on KVM conversion hosts",
true,
ConfigKey.Kind.CSV,
null);
@Inject @Inject
private AgentManager agentManager; private AgentManager agentManager;
@Inject @Inject
@ -282,6 +314,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
private ImageStoreDao imageStoreDao; private ImageStoreDao imageStoreDao;
@Inject @Inject
private DataStoreManager dataStoreManager; private DataStoreManager dataStoreManager;
@Inject
private ImportVmTasksManager importVmTasksManager;
protected Gson gson; protected Gson gson;
@ -557,11 +591,12 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
} }
if (storagePool == null) { if (storagePool == null) {
List<StoragePoolVO> pools = primaryDataStoreDao.listPoolsByCluster(cluster.getId()); Set<StoragePoolVO> pools = new HashSet<>(primaryDataStoreDao.listPoolsByCluster(cluster.getId()));
pools.addAll(primaryDataStoreDao.listByDataCenterId(zone.getId())); pools.addAll(primaryDataStoreDao.listByDataCenterId(zone.getId()));
boolean isNameUuid = StringUtils.isNotBlank(dsName) && UuidUtils.isUuid(dsName);
for (StoragePool pool : pools) { for (StoragePool pool : pools) {
String searchPoolParam = StringUtils.isNotBlank(dsPath) ? dsPath : dsName; 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)) { volumeApiService.doesStoragePoolSupportDiskOffering(pool, diskOffering)) {
storagePool = pool; storagePool = pool;
break; break;
@ -1418,6 +1453,33 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
return responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVm).get(0); return responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVm).get(0);
} }
protected void checkExtraParamsAllowed(String extraParams) {
if (StringUtils.isBlank(extraParams)) {
return;
}
if (BooleanUtils.isFalse(ConvertVmwareInstanceToKvmExtraParamsAllowed.value())) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
"Extra parameters for Vmware to KVM conversion are disabled by the administrator");
}
String allowedParamsStr = ConvertVmwareInstanceToKvmExtraParamsAllowedList.value();
if (StringUtils.isBlank(allowedParamsStr)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
"Extra parameters for Vmware to KVM conversion are enabled but the allowed list of parameters is empty");
}
List<String> allowedParams = Arrays.asList(allowedParamsStr.split(","));
List<String> sanitizedParams = Arrays.asList(extraParams.split(" "))
.stream()
.filter(x -> x.startsWith("-"))
.map(s -> s.replaceFirst("^-+", "").trim()) //Remove the starting hyphens as in --X or -x
.collect(Collectors.toList());
for (String param : sanitizedParams) {
if (!allowedParams.contains(param)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR,
String.format("The parameter %s is not allowed by the administrator", param));
}
}
}
private long getUserIdForImportInstance(Account owner) { private long getUserIdForImportInstance(Account owner) {
long userId = CallContext.current().getCallingUserId(); long userId = CallContext.current().getCallingUserId();
List<UserVO> userVOs = userDao.listByAccount(owner.getAccountId()); List<UserVO> userVOs = userDao.listByAccount(owner.getAccountId());
@ -1513,6 +1575,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
String source = cmd.getImportSource().toUpperCase(); String source = cmd.getImportSource().toUpperCase();
ImportSource importSource = Enum.valueOf(ImportSource.class, source); ImportSource importSource = Enum.valueOf(ImportSource.class, source);
if (ImportSource.VMWARE == importSource || ImportSource.UNMANAGED == importSource) { if (ImportSource.VMWARE == importSource || ImportSource.UNMANAGED == importSource) {
checkExtraParamsAllowed(cmd.getExtraParams());
return baseImportInstance(cmd); return baseImportInstance(cmd);
} else { } else {
return importKvmInstance(cmd); return importKvmInstance(cmd);
@ -1621,6 +1684,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
Long convertInstanceHostId = cmd.getConvertInstanceHostId(); Long convertInstanceHostId = cmd.getConvertInstanceHostId();
Long importInstanceHostId = cmd.getImportInstanceHostId(); Long importInstanceHostId = cmd.getImportInstanceHostId();
Long convertStoragePoolId = cmd.getConvertStoragePoolId(); Long convertStoragePoolId = cmd.getConvertStoragePoolId();
String extraParams = cmd.getExtraParams();
boolean forceConvertToPool = cmd.getForceConvertToPool();
if ((existingVcenterId == null && vcenter == null) || (existingVcenterId != null && vcenter != null)) { if ((existingVcenterId == null && vcenter == null) || (existingVcenterId != null && vcenter != null)) {
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, 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"); "Please set all the information for a vCenter IP/Name, datacenter, username and password");
} }
checkConversionStoragePool(convertStoragePoolId, forceConvertToPool);
checkExtraParamsAllowed(extraParams);
if (existingVcenterId != null) { if (existingVcenterId != null) {
VmwareDatacenterVO existingDC = vmwareDatacenterDao.findById(existingVcenterId); VmwareDatacenterVO existingDC = vmwareDatacenterDao.findById(existingVcenterId);
if (existingDC == null) { if (existingDC == null) {
@ -1648,6 +1717,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
UnmanagedInstanceTO sourceVMwareInstance = null; UnmanagedInstanceTO sourceVMwareInstance = null;
DataStoreTO temporaryConvertLocation = null; DataStoreTO temporaryConvertLocation = null;
String ovfTemplateOnConvertLocation = null; String ovfTemplateOnConvertLocation = null;
ImportVmTask importVMTask = null;
try { try {
HostVO convertHost = selectKVMHostForConversionInCluster(destinationCluster, convertInstanceHostId); HostVO convertHost = selectKVMHostForConversionInCluster(destinationCluster, convertInstanceHostId);
HostVO importHost = selectKVMHostForImportingInCluster(destinationCluster, importInstanceHostId); HostVO importHost = selectKVMHostForImportingInCluster(destinationCluster, importInstanceHostId);
@ -1656,12 +1726,21 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
"instance {} from VMware to KVM ", convertHost, sourceVMName); "instance {} from VMware to KVM ", convertHost, sourceVMName);
temporaryConvertLocation = selectInstanceConversionTemporaryLocation( temporaryConvertLocation = selectInstanceConversionTemporaryLocation(
destinationCluster, convertHost, convertStoragePoolId); destinationCluster, convertHost, importHost, convertStoragePoolId, forceConvertToPool);
List<StoragePoolVO> convertStoragePools = findInstanceConversionStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap); List<StoragePoolVO> convertStoragePools = findInstanceConversionDestinationStoragePoolsInCluster(destinationCluster, serviceOffering, dataDiskOfferingMap, temporaryConvertLocation, forceConvertToPool);
long importStartTime = System.currentTimeMillis(); long importStartTime = System.currentTimeMillis();
importVMTask = importVmTasksManager.createImportVMTaskRecord(zone, owner, userId, displayName, vcenter, datacenterName, sourceVMName,
convertHost, importHost);
importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, CloningInstance);
// sourceVMwareInstance could be a cloned instance from sourceVMName, of the sourceVMName itself if its powered off.
// isClonedInstance indicates if the VM is a clone of sourceVMName
Pair<UnmanagedInstanceTO, Boolean> sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName); Pair<UnmanagedInstanceTO, Boolean> sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName);
sourceVMwareInstance = sourceInstanceDetails.first(); sourceVMwareInstance = sourceInstanceDetails.first();
isClonedInstance = sourceInstanceDetails.second(); isClonedInstance = sourceInstanceDetails.second();
boolean isWindowsVm = sourceVMwareInstance.getOperatingSystem().toLowerCase().contains("windows"); boolean isWindowsVm = sourceVMwareInstance.getOperatingSystem().toLowerCase().contains("windows");
if (isWindowsVm) { if (isWindowsVm) {
checkConversionSupportOnHost(convertHost, sourceVMName, true); checkConversionSupportOnHost(convertHost, sourceVMName, true);
@ -1672,22 +1751,25 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
if (cmd.getForceMsToImportVmFiles() || !conversionSupportAnswer.isOvfExportSupported()) { if (cmd.getForceMsToImportVmFiles() || !conversionSupportAnswer.isOvfExportSupported()) {
// Uses MS for OVF export to temporary conversion location // Uses MS for OVF export to temporary conversion location
int noOfThreads = UnmanagedVMsManager.ThreadsOnMSToImportVMwareVMFiles.value(); int noOfThreads = UnmanagedVMsManager.ThreadsOnMSToImportVMwareVMFiles.value();
importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, ConvertingInstance);
ovfTemplateOnConvertLocation = createOvfTemplateOfSourceVmwareUnmanagedInstance( ovfTemplateOnConvertLocation = createOvfTemplateOfSourceVmwareUnmanagedInstance(
vcenter, datacenterName, username, password, clusterName, sourceHostName, vcenter, datacenterName, username, password, clusterName, sourceHostName,
sourceVMwareInstance.getName(), temporaryConvertLocation, noOfThreads); sourceVMwareInstance.getName(), temporaryConvertLocation, noOfThreads);
convertedInstance = convertVmwareInstanceToKVMWithOVFOnConvertLocation(sourceVMName, convertedInstance = convertVmwareInstanceToKVMWithOVFOnConvertLocation(sourceVMName,
sourceVMwareInstance, convertHost, importHost, convertStoragePools, sourceVMwareInstance, convertHost, importHost, convertStoragePools,
serviceOffering, dataDiskOfferingMap, temporaryConvertLocation, serviceOffering, dataDiskOfferingMap, temporaryConvertLocation,
ovfTemplateOnConvertLocation); ovfTemplateOnConvertLocation, forceConvertToPool, extraParams);
} else { } else {
// Uses KVM Host for OVF export to temporary conversion location, through ovftool // Uses KVM Host for OVF export to temporary conversion location, through ovftool
importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, ConvertingInstance);
convertedInstance = convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation( convertedInstance = convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation(
sourceVMName, sourceVMwareInstance, convertHost, importHost, sourceVMName, sourceVMwareInstance, convertHost, importHost,
convertStoragePools, serviceOffering, dataDiskOfferingMap, convertStoragePools, serviceOffering, dataDiskOfferingMap,
temporaryConvertLocation, vcenter, username, password, datacenterName); temporaryConvertLocation, vcenter, username, password, datacenterName, forceConvertToPool, extraParams);
} }
sanitizeConvertedInstance(convertedInstance, sourceVMwareInstance); sanitizeConvertedInstance(convertedInstance, sourceVMwareInstance);
importVmTasksManager.updateImportVMTaskStep(importVMTask, zone, owner, convertHost, importHost, null, Importing);
UserVm userVm = importVirtualMachineInternal(convertedInstance, null, zone, destinationCluster, null, UserVm userVm = importVirtualMachineInternal(convertedInstance, null, zone, destinationCluster, null,
template, displayName, hostName, caller, owner, userId, template, displayName, hostName, caller, owner, userId,
serviceOffering, dataDiskOfferingMap, serviceOffering, dataDiskOfferingMap,
@ -1696,6 +1778,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
long timeElapsedInSecs = (System.currentTimeMillis() - importStartTime) / 1000; 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", 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())); 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; return userVm;
} catch (CloudRuntimeException e) { } catch (CloudRuntimeException e) {
logger.error(String.format("Error importing VM: %s", e.getMessage()), 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, private void checkNetworkingBeforeConvertingVmwareInstance(DataCenter zone, Account owner, String displayName,
String hostName, UnmanagedInstanceTO sourceVMwareInstance, String hostName, UnmanagedInstanceTO sourceVMwareInstance,
Map<String, Long> nicNetworkMap, Map<String, Long> nicNetworkMap,
@ -1953,20 +2061,25 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
String sourceVM, UnmanagedInstanceTO sourceVMwareInstance, HostVO convertHost, String sourceVM, UnmanagedInstanceTO sourceVMwareInstance, HostVO convertHost,
HostVO importHost, List<StoragePoolVO> convertStoragePools, HostVO importHost, List<StoragePoolVO> convertStoragePools,
ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap, ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap,
DataStoreTO temporaryConvertLocation, String 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", logger.debug("Delegating the conversion of instance {} from VMware to KVM to the host {} using OVF {} on conversion datastore",
sourceVM, convertHost, ovfTemplateDirConvertLocation); sourceVM, convertHost, ovfTemplateDirConvertLocation);
RemoteInstanceTO remoteInstanceTO = new RemoteInstanceTO(sourceVM); RemoteInstanceTO remoteInstanceTO = new RemoteInstanceTO(sourceVM);
List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap); List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap);
ConvertInstanceCommand cmd = new ConvertInstanceCommand(remoteInstanceTO, 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; int timeoutSeconds = UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.value() * 60 * 60;
cmd.setWait(timeoutSeconds); cmd.setWait(timeoutSeconds);
return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM, return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM,
remoteInstanceTO, destinationStoragePools, temporaryConvertLocation); remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, forceConvertToPool);
} }
private UnmanagedInstanceTO convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation( private UnmanagedInstanceTO convertVmwareInstanceToKVMAfterExportingOVFToConvertLocation(
@ -1974,14 +2087,13 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
HostVO importHost, List<StoragePoolVO> convertStoragePools, HostVO importHost, List<StoragePoolVO> convertStoragePools,
ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap, ServiceOfferingVO serviceOffering, Map<String, Long> dataDiskOfferingMap,
DataStoreTO temporaryConvertLocation, String vcenterHost, String vcenterUsername, 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); 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); RemoteInstanceTO remoteInstanceTO = new RemoteInstanceTO(sourceVMwareInstance.getName(), sourceVMwareInstance.getPath(), vcenterHost, vcenterUsername, vcenterPassword, datacenterName);
List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap); List<String> destinationStoragePools = selectInstanceConversionStoragePools(convertStoragePools, sourceVMwareInstance.getDisks(), serviceOffering, dataDiskOfferingMap);
ConvertInstanceCommand cmd = new ConvertInstanceCommand(remoteInstanceTO, 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; int timeoutSeconds = UnmanagedVMsManager.ConvertVmwareInstanceToKvmTimeout.value() * 60 * 60;
cmd.setWait(timeoutSeconds); cmd.setWait(timeoutSeconds);
int noOfThreads = UnmanagedVMsManager.ThreadsOnKVMHostToImportVMwareVMFiles.value(); int noOfThreads = UnmanagedVMsManager.ThreadsOnKVMHostToImportVMwareVMFiles.value();
@ -1990,16 +2102,19 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
noOfThreads = sourceVMwareInstance.getDisks().size(); noOfThreads = sourceVMwareInstance.getDisks().size();
} }
cmd.setThreadsCountToExportOvf(noOfThreads); cmd.setThreadsCountToExportOvf(noOfThreads);
if (StringUtils.isNotBlank(extraParams)) {
cmd.setExtraParams(extraParams);
}
return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM, return convertAndImportToKVM(cmd, convertHost, importHost, sourceVM,
remoteInstanceTO, destinationStoragePools, temporaryConvertLocation); remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, forceConvertToPool);
} }
private UnmanagedInstanceTO convertAndImportToKVM(ConvertInstanceCommand convertInstanceCommand, HostVO convertHost, HostVO importHost, private UnmanagedInstanceTO convertAndImportToKVM(ConvertInstanceCommand convertInstanceCommand, HostVO convertHost, HostVO importHost,
String sourceVM, String sourceVM,
RemoteInstanceTO remoteInstanceTO, RemoteInstanceTO remoteInstanceTO,
List<String> destinationStoragePools, List<String> destinationStoragePools,
DataStoreTO temporaryConvertLocation) { DataStoreTO temporaryConvertLocation,
boolean forceConvertToPool) {
Answer convertAnswer; Answer convertAnswer;
try { try {
convertAnswer = agentManager.send(convertHost.getId(), convertInstanceCommand); convertAnswer = agentManager.send(convertHost.getId(), convertInstanceCommand);
@ -2021,7 +2136,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
try { try {
ImportConvertedInstanceCommand importCmd = new ImportConvertedInstanceCommand( ImportConvertedInstanceCommand importCmd = new ImportConvertedInstanceCommand(
remoteInstanceTO, destinationStoragePools, temporaryConvertLocation, remoteInstanceTO, destinationStoragePools, temporaryConvertLocation,
((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid()); ((ConvertInstanceAnswer)convertAnswer).getTemporaryConvertUuid(), forceConvertToPool);
importAnswer = agentManager.send(importHost.getId(), importCmd); importAnswer = agentManager.send(importHost.getId(), importCmd);
} catch (AgentUnavailableException | OperationTimedoutException e) { } catch (AgentUnavailableException | OperationTimedoutException e) {
String err = String.format( String err = String.format(
@ -2042,18 +2157,24 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
return ((ImportConvertedInstanceAnswer) importAnswer).getConvertedInstance(); return ((ImportConvertedInstanceAnswer) importAnswer).getConvertedInstance();
} }
private List<StoragePoolVO> findInstanceConversionStoragePoolsInCluster( private List<StoragePoolVO> findInstanceConversionDestinationStoragePoolsInCluster(
Cluster destinationCluster, ServiceOfferingVO serviceOffering, Cluster destinationCluster, ServiceOfferingVO serviceOffering,
Map<String, Long> dataDiskOfferingMap Map<String, Long> dataDiskOfferingMap,
) { DataStoreTO temporaryConvertLocation, boolean forceConvertToPool) {
List<StoragePoolVO> pools = new ArrayList<>(); List<StoragePoolVO> poolsList;
pools.addAll(primaryDataStoreDao.findClusterWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem)); if (!forceConvertToPool) {
Set<StoragePoolVO> pools = new HashSet<>(primaryDataStoreDao.findClusterWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
pools.addAll(primaryDataStoreDao.findZoneWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getDataCenterId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem)); pools.addAll(primaryDataStoreDao.findZoneWideStoragePoolsByHypervisorAndPoolType(destinationCluster.getDataCenterId(), Hypervisor.HypervisorType.KVM, Storage.StoragePoolType.NetworkFilesystem));
if (pools.isEmpty()) { if (pools.isEmpty()) {
String msg = String.format("Cannot find suitable storage pools in the cluster %s for the conversion", destinationCluster.getName()); String msg = String.format("Cannot find suitable storage pools in the cluster %s for the conversion", destinationCluster.getName());
logger.error(msg); logger.error(msg);
throw new CloudRuntimeException(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) { if (serviceOffering.getDiskOfferingId() != null) {
DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId());
@ -2062,7 +2183,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
logger.error(msg); logger.error(msg);
throw new CloudRuntimeException(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()); 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); logger.error(msg);
throw new CloudRuntimeException(msg); throw new CloudRuntimeException(msg);
@ -2075,14 +2196,14 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
logger.error(msg); logger.error(msg);
throw new CloudRuntimeException(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()); String msg = String.format("Cannot find suitable storage pool for disk offering %s", diskOffering.getName());
logger.error(msg); logger.error(msg);
throw new CloudRuntimeException(msg); throw new CloudRuntimeException(msg);
} }
} }
return pools; return poolsList;
} }
private StoragePoolVO getStoragePoolWithTags(List<StoragePoolVO> pools, String tags) { private StoragePoolVO getStoragePoolWithTags(List<StoragePoolVO> pools, String tags) {
@ -2128,11 +2249,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
throw new CloudRuntimeException(msg); throw new CloudRuntimeException(msg);
} }
protected DataStoreTO selectInstanceConversionTemporaryLocation(Cluster destinationCluster, private void checkBeforeSelectingTemporaryConversionStoragePool(StoragePoolVO selectedStoragePool, Long convertStoragePoolId, Cluster destinationCluster, HostVO convertHost) {
HostVO convertHost,
Long convertStoragePoolId) {
if (convertStoragePoolId != null) {
StoragePoolVO selectedStoragePool = primaryDataStoreDao.findById(convertStoragePoolId);
if (selectedStoragePool == null) { if (selectedStoragePool == null) {
logFailureAndThrowException(String.format("Cannot find a storage pool with ID %s", convertStoragePoolId)); logFailureAndThrowException(String.format("Cannot find a storage pool with ID %s", convertStoragePoolId));
} }
@ -2145,13 +2262,12 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
logFailureAndThrowException(String.format("Cannot use the storage pool %s for the instance conversion as " + 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())); "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 { 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(); long zoneId = destinationCluster.getDataCenterId();
ImageStoreVO imageStore = imageStoreDao.findOneByZoneAndProtocol(zoneId, "nfs"); ImageStoreVO imageStore = imageStoreDao.findOneByZoneAndProtocol(zoneId, "nfs");
if (imageStore == null) { if (imageStore == null) {
@ -2161,6 +2277,35 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
DataStore dataStore = dataStoreManager.getDataStore(imageStore.getId(), DataStoreRole.Image); DataStore dataStore = dataStoreManager.getDataStore(imageStore.getId(), DataStoreRole.Image);
return dataStore.getTO(); 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<String, String> createParamsForTemplateFromVmwareVmMigration(String vcenterHost, String datacenterName, protected Map<String, String> createParamsForTemplateFromVmwareVmMigration(String vcenterHost, String datacenterName,
@ -2186,6 +2331,7 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
cmdList.add(UnmanageVMInstanceCmd.class); cmdList.add(UnmanageVMInstanceCmd.class);
cmdList.add(ListVmsForImportCmd.class); cmdList.add(ListVmsForImportCmd.class);
cmdList.add(ImportVmCmd.class); cmdList.add(ImportVmCmd.class);
cmdList.add(ListImportVMTasksCmd.class);
return cmdList; return cmdList;
} }
@ -2865,7 +3011,9 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager {
RemoteKvmInstanceDisksCopyTimeout, RemoteKvmInstanceDisksCopyTimeout,
ConvertVmwareInstanceToKvmTimeout, ConvertVmwareInstanceToKvmTimeout,
ThreadsOnMSToImportVMwareVMFiles, ThreadsOnMSToImportVMwareVMFiles,
ThreadsOnKVMHostToImportVMwareVMFiles ThreadsOnKVMHostToImportVMwareVMFiles,
ConvertVmwareInstanceToKvmExtraParamsAllowed,
ConvertVmwareInstanceToKvmExtraParamsAllowedList
}; };
} }
} }

View File

@ -37,4 +37,6 @@
<bean id="vmImportService" class="org.apache.cloudstack.vm.UnmanagedVMsManagerImpl" /> <bean id="vmImportService" class="org.apache.cloudstack.vm.UnmanagedVMsManagerImpl" />
<bean id="importVmTasksManager" class="org.apache.cloudstack.vm.ImportVmTasksManagerImpl" />
</beans> </beans>

View File

@ -39,6 +39,7 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import com.cloud.offering.DiskOffering; import com.cloud.offering.DiskOffering;
import com.cloud.vm.ImportVMTaskVO;
import org.apache.cloudstack.api.ResponseGenerator; import org.apache.cloudstack.api.ResponseGenerator;
import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ResponseObject;
import org.apache.cloudstack.api.ServerApiException; 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.orchestration.service.VolumeOrchestrationService;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; import org.apache.cloudstack.engine.subsystem.api.storage.DataStore;
import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; 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.framework.config.dao.ConfigurationDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; import org.apache.cloudstack.storage.datastore.db.ImageStoreDao;
import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; import org.apache.cloudstack.storage.datastore.db.ImageStoreVO;
@ -236,6 +238,8 @@ public class UnmanagedVMsManagerImplTest {
private DataStoreManager dataStoreManager; private DataStoreManager dataStoreManager;
@Mock @Mock
private StoragePoolHostDao storagePoolHostDao; private StoragePoolHostDao storagePoolHostDao;
@Mock
private ImportVmTasksManager importVmTasksManager;
@Mock @Mock
private VMInstanceVO virtualMachine; private VMInstanceVO virtualMachine;
@ -244,8 +248,15 @@ public class UnmanagedVMsManagerImplTest {
@Mock @Mock
DeploymentPlanningManager deploymentPlanningManager; DeploymentPlanningManager deploymentPlanningManager;
@Mock @Mock
ImportVMTaskVO importVMTaskVO;
@Mock
private VMInstanceDetailsDao vmInstanceDetailsDao; private VMInstanceDetailsDao vmInstanceDetailsDao;
@Mock
private ConfigKey<Boolean> configKeyMockParamsAllowed;
@Mock
private ConfigKey<String> configKeyMockParamsAllowedList;
private static final long virtualMachineId = 1L; private static final long virtualMachineId = 1L;
private AutoCloseable closeable; private AutoCloseable closeable;
@ -720,7 +731,6 @@ public class UnmanagedVMsManagerImplTest {
when(dataStore.getTO()).thenReturn(dataStoreTO); when(dataStore.getTO()).thenReturn(dataStoreTO);
StoragePoolVO destPool = mock(StoragePoolVO.class); StoragePoolVO destPool = mock(StoragePoolVO.class);
when(destPool.getUuid()).thenReturn(UUID.randomUUID().toString());
when(destPool.getDataCenterId()).thenReturn(zoneId); when(destPool.getDataCenterId()).thenReturn(zoneId);
when(destPool.getClusterId()).thenReturn(null); when(destPool.getClusterId()).thenReturn(null);
when(destPool.getPoolType()).thenReturn(Storage.StoragePoolType.NetworkFilesystem); 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(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); when(volumeApiService.doesStoragePoolSupportDiskOffering(any(StoragePool.class), any(DiskOffering.class))).thenReturn(true);
ConvertInstanceAnswer convertInstanceAnswer = mock(ConvertInstanceAnswer.class); ConvertInstanceAnswer convertInstanceAnswer = mock(ConvertInstanceAnswer.class);
@ -895,7 +908,7 @@ public class UnmanagedVMsManagerImplTest {
long poolId = 1L; long poolId = 1L;
when(primaryDataStoreDao.findById(poolId)).thenReturn(null); when(primaryDataStoreDao.findById(poolId)).thenReturn(null);
unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId); unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
} }
@Test(expected = CloudRuntimeException.class) @Test(expected = CloudRuntimeException.class)
@ -906,7 +919,7 @@ public class UnmanagedVMsManagerImplTest {
when(pool.getScope()).thenReturn(ScopeType.CLUSTER); when(pool.getScope()).thenReturn(ScopeType.CLUSTER);
when(pool.getClusterId()).thenReturn(100L); when(pool.getClusterId()).thenReturn(100L);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); 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); HostVO host = mock(HostVO.class);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(host.getClusterId()).thenReturn(2L); 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); StoragePoolVO pool = mock(StoragePoolVO.class);
when(pool.getScope()).thenReturn(ScopeType.HOST); when(pool.getScope()).thenReturn(ScopeType.HOST);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId); unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
} }
@Test(expected = CloudRuntimeException.class) @Test(expected = CloudRuntimeException.class)
@ -943,14 +956,14 @@ public class UnmanagedVMsManagerImplTest {
when(pool.getClusterId()).thenReturn(1L); when(pool.getClusterId()).thenReturn(1L);
when(primaryDataStoreDao.findById(poolId)).thenReturn(pool); when(primaryDataStoreDao.findById(poolId)).thenReturn(pool);
when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD); when(pool.getPoolType()).thenReturn(Storage.StoragePoolType.RBD);
unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, poolId); unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, poolId, false);
} }
@Test(expected = CloudRuntimeException.class) @Test(expected = CloudRuntimeException.class)
public void testSelectInstanceConversionTemporaryLocationNoPoolAvailable() { public void testSelectInstanceConversionTemporaryLocationNoPoolAvailable() {
ClusterVO cluster = getClusterForTests(); ClusterVO cluster = getClusterForTests();
when(imageStoreDao.findOneByZoneAndProtocol(anyLong(), anyString())).thenReturn(null); when(imageStoreDao.findOneByZoneAndProtocol(anyLong(), anyString())).thenReturn(null);
unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null); unmanagedVMsManager.selectInstanceConversionTemporaryLocation(cluster, null, null, null, false);
} }
@Test @Test
@ -1220,4 +1233,81 @@ public class UnmanagedVMsManagerImplTest {
Assert.fail("Exception encountered: " + e.getMessage()); 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");
}
} }

View File

@ -272,7 +272,8 @@ known_categories = {
'Extension' : 'Extension', 'Extension' : 'Extension',
'Extensions' : 'Extension', 'Extensions' : 'Extension',
'CustomAction' : 'Extension', 'CustomAction' : 'Extension',
'CustomActions' : 'Extension' 'CustomActions' : 'Extension',
'ImportVmTask': 'Import VM Task'
} }

View File

@ -1089,6 +1089,7 @@
"label.forcks": "For CKS", "label.forcks": "For CKS",
"label.forbidden": "Forbidden", "label.forbidden": "Forbidden",
"label.forced": "Force", "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.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.update.os.type": "Force update OS type",
"label.force.stop": "Force stop", "label.force.stop": "Force stop",
@ -3732,8 +3733,10 @@
"message.select.deselect.desired.options": "Please select / deselect the desired options", "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.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.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.disk.offering": "Please select a disk offering for disk",
"message.select.end.date.and.time": "Select an end date & time.", "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.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.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.", "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.nic.network": "Please select a Network for NIC",
"message.select.security.groups": "Please select security group(s) for your new Instance.", "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.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.volume.to.continue": "Please select a volume to continue.",
"message.select.vm.to.continue": "Please select an Instance to continue.", "message.select.vm.to.continue": "Please select an Instance to continue.",
"message.select.zone.description": "Select type of Zone basic/advanced.", "message.select.zone.description": "Select type of Zone basic/advanced.",

View File

@ -152,6 +152,12 @@
</a-row> </a-row>
</a-radio-group> </a-radio-group>
</a-form-item> </a-form-item>
<a-form-item name="forceconverttopool" ref="forceconverttopool" v-if="selectedVmwareVcenter">
<template #label>
<tooltip-label :title="$t('label.force.convert.to.pool')" :tooltip="apiParams.forceconverttopool.description"/>
</template>
<a-switch v-model:checked="form.forceconverttopool" @change="onForceConvertToPoolChange" />
</a-form-item>
<a-form-item name="converthostid" ref="converthostid"> <a-form-item name="converthostid" ref="converthostid">
<check-box-select-pair <check-box-select-pair
layout="vertical" layout="vertical"
@ -179,17 +185,21 @@
<a-form-item name="convertstorageoption" ref="convertstorageoption"> <a-form-item name="convertstorageoption" ref="convertstorageoption">
<check-box-select-pair <check-box-select-pair
layout="vertical" layout="vertical"
style="margin-bottom: 5px"
v-if="cluster.hypervisortype === 'KVM' && selectedVmwareVcenter" v-if="cluster.hypervisortype === 'KVM' && selectedVmwareVcenter"
:resourceKey="cluster.id" :resourceKey="cluster.id"
:selectOptions="storageOptionsForConversion" :selectOptions="storageOptionsForConversion"
:checkBoxLabel="$t('message.select.temporary.storage.instance.conversion')" :checkBoxLabel="switches.forceConvertToPool ? $t('message.select.destination.storage.instance.conversion') : $t('message.select.temporary.storage.instance.conversion')"
:defaultCheckBoxValue="false" :defaultCheckBoxValue="false"
:reversed="false" :reversed="false"
@handle-checkselectpair-change="updateSelectedStorageOptionForConversion" @handle-checkselectpair-change="updateSelectedStorageOptionForConversion"
/> />
</a-form-item> </a-form-item>
<a-form-item v-if="showStoragePoolsForConversion" name="convertstoragepool" ref="convertstoragepool" :label="$t('label.storagepool')"> <a-form-item
v-if="showStoragePoolsForConversion"
name="convertstoragepool"
ref="convertstoragepool"
:label="$t('label.storagepool')"
>
<a-select <a-select
v-model:value="form.convertstoragepoolid" v-model:value="form.convertstoragepoolid"
defaultActiveFirstOption defaultActiveFirstOption
@ -204,6 +214,18 @@
</a-select-option> </a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item name="extraparams" ref="extraparams">
<a-checkbox
v-if="cluster.hypervisortype === 'KVM' && selectedVmwareVcenter && vmwareToKvmExtraParamsAllowed"
v-model:checked="vmwareToKvmExtraParamsSelected">
{{ $t('message.select.extra.parameters.for.instance.conversion') }}
</a-checkbox>
<a-input
v-if="vmwareToKvmExtraParamsSelected"
v-model:value="vmwareToKvmExtraParams"
:placeholder="$t('label.extra')"
/>
</a-form-item>
<a-form-item name="forcemstoimportvmfiles" ref="forcemstoimportvmfiles" v-if="selectedVmwareVcenter"> <a-form-item name="forcemstoimportvmfiles" ref="forcemstoimportvmfiles" v-if="selectedVmwareVcenter">
<template #label> <template #label>
<tooltip-label :title="$t('label.force.ms.to.import.vm.files')" :tooltip="apiParams.forcemstoimportvmfiles.description"/> <tooltip-label :title="$t('label.force.ms.to.import.vm.files')" :tooltip="apiParams.forcemstoimportvmfiles.description"/>
@ -529,7 +551,10 @@ export default {
title: this.$t('label.rootdisk') title: this.$t('label.rootdisk')
} }
], ],
selectedRootDiskSources: [] selectedRootDiskSources: [],
vmwareToKvmExtraParamsAllowed: false,
vmwareToKvmExtraParamsSelected: false,
vmwareToKvmExtraParams: ''
} }
}, },
beforeCreate () { beforeCreate () {
@ -724,6 +749,7 @@ export default {
migrateallowed: this.switches.migrateAllowed, migrateallowed: this.switches.migrateAllowed,
forced: this.switches.forced, forced: this.switches.forced,
forcemstoimportvmfiles: this.switches.forceMsToImportVmFiles, forcemstoimportvmfiles: this.switches.forceMsToImportVmFiles,
forceconverttopool: this.switches.forceConvertToPool,
domainid: null, domainid: null,
account: null account: null
}) })
@ -749,6 +775,20 @@ export default {
if (this.resource?.disk?.length > 1) { if (this.resource?.disk?.length > 1) {
this.updateSelectedRootDisk() this.updateSelectedRootDisk()
} }
this.fetchVmwareToKVMExtraConfigsSetting()
},
fetchVmwareToKVMExtraConfigsSetting () {
const params = {
name: 'convert.vmware.instance.to.kvm.extra.params.allowed'
}
getAPI('listConfigurations', params).then(json => {
if (json.listconfigurationsresponse.configuration !== null) {
const config = json.listconfigurationsresponse.configuration[0]
if (config && config.name === params.name) {
this.vmwareToKvmExtraParamsAllowed = config.value
}
}
})
}, },
getMeta (obj, metaKeys) { getMeta (obj, metaKeys) {
var meta = [] var meta = []
@ -1027,20 +1067,23 @@ export default {
} }
}, },
resetStorageOptionsForConversion () { resetStorageOptionsForConversion () {
this.storageOptionsForConversion = [ this.storageOptionsForConversion = this.switches.forceConvertToPool ? [] : [{
{
id: 'secondary', id: 'secondary',
name: 'Secondary Storage' name: 'Secondary Storage'
}, { }]
this.storageOptionsForConversion.push({
id: 'primary', id: 'primary',
name: 'Primary Storage' name: 'Primary Storage'
} })
]
}, },
onSelectRootDisk (val) { onSelectRootDisk (val) {
this.selectedRootDiskIndex = val this.selectedRootDiskIndex = val
this.updateSelectedRootDisk() this.updateSelectedRootDisk()
}, },
onForceConvertToPoolChange (val) {
this.switches.forceConvertToPool = val
this.resetStorageOptionsForConversion()
},
updateSelectedRootDisk () { updateSelectedRootDisk () {
var rootDisk = this.resource.disk[this.selectedRootDiskIndex] var rootDisk = this.resource.disk[this.selectedRootDiskIndex]
rootDisk.size = rootDisk.capacity / (1024 * 1024 * 1024) rootDisk.size = rootDisk.capacity / (1024 * 1024 * 1024)
@ -1156,7 +1199,13 @@ export default {
if (this.selectedStoragePoolForConversion) { if (this.selectedStoragePoolForConversion) {
params.convertinstancepoolid = this.selectedStoragePoolForConversion params.convertinstancepoolid = this.selectedStoragePoolForConversion
} }
if (this.vmwareToKvmExtraParams) {
params.extraparams = this.vmwareToKvmExtraParams
}
params.forcemstoimportvmfiles = values.forcemstoimportvmfiles params.forcemstoimportvmfiles = values.forcemstoimportvmfiles
if (values.forceconverttopool) {
params.forceconverttopool = values.forceconverttopool
}
} }
var keys = ['hostname', 'domainid', 'projectid', 'account', 'migrateallowed', 'forced', 'forcemstoimportvmfiles'] var keys = ['hostname', 'domainid', 'projectid', 'account', 'migrateallowed', 'forced', 'forcemstoimportvmfiles']
if (this.templateType !== 'auto') { if (this.templateType !== 'auto') {

View File

@ -0,0 +1,150 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
<template>
<a-card class="instances-card">
<template #title>
Import VM Tasks
<a-tooltip :title="'Running Import VM Tasks'">
<info-circle-outlined />
</a-tooltip>
<a-button
style="margin-left: 12px; margin-top: 4px"
:loading="loading"
size="small"
shape="round"
@click="fetchData()">
<template #icon><reload-outlined /></template>
</a-button>
<a-select
:placeholder="$t('label.filterby')"
:value="importVmTasksFilterValue"
style="min-width: 100px; margin-left: 10px; margin-bottom: 5px"
size=small
@change="onFilterChange"
showSearch
optionFilterProp="label"
:filterOption="(input, option) => {
return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0
}"
>
<template #suffixIcon><filter-outlined class="ant-select-suffix" /></template>
<a-select-option
v-for="filter in filters"
:key="filter"
:label="$t('label.' + filter)"
>
{{ $t('label.' + filter) }}
</a-select-option>
</a-select>
</template>
<a-table
:data-source="tasks"
:columns="columns">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'convertinstancehostid'">
<router-link :to="{ path: '/host/' + record.convertinstancehostid }">{{ record.convertinstancehostname }}</router-link>
</template>
<template v-else-if="column.key === 'displayname'">
<router-link v-if="record.virtualmachineid" :to="{ path: '/vm/' + record.virtualmachineid }">{{ record.displayname }}</router-link>
<span v-else>{{ record.displayname }}</span>
</template>
</template>
</a-table>
</a-card>
</template>
<script>
export default {
name: 'ImportVmTasks',
components: {
},
props: {
tasks: {
type: Array,
required: true
},
loading: {
type: Boolean,
required: false
}
},
data () {
const columns = [
{
key: 'displayname',
title: 'VM Display Name',
dataIndex: 'displayname'
},
{
key: 'convertinstancehostid',
title: 'Conversion Host',
dataIndex: 'convertinstancehostid'
},
{
key: 'step',
title: 'Current Step',
dataIndex: 'step'
},
{
key: 'stepduration',
title: 'Current Step Duration',
dataIndex: 'stepduration'
},
{
key: 'description',
title: 'Description',
dataIndex: 'description'
},
{
key: 'duration',
title: 'Total Duration',
dataIndex: 'duration'
},
{
key: 'sourcevmname',
title: 'Source VM Name',
dataIndex: 'sourcevmname'
},
{
key: 'vcenter',
title: 'vCenter',
dataIndex: 'vcenter'
},
{
key: 'datacentername',
title: 'Datacenter Name',
dataIndex: 'datacentername'
}
]
return {
columns,
filters: ['running', 'completed'],
filterValue: 'running'
}
},
methods: {
fetchData () {
this.$emit('fetch-import-vm-tasks', this.filterValue)
},
onFilterChange (e) {
this.filterValue = e
this.fetchData()
}
}
}
</script>

View File

@ -333,6 +333,9 @@
</a-col> </a-col>
</a-row> </a-row>
<a-divider /> <a-divider />
<a-tabs v-model:activeKey="activeTabKey" @change="onTabChange">
<a-tab-pane :key=1 tab="Instances Listing">
<a-row :gutter="12"> <a-row :gutter="12">
<a-col v-if="!isDiskImport" :md="24" :lg="(!isMigrateFromVmware && showManagedInstances) ? 12 : 24"> <a-col v-if="!isDiskImport" :md="24" :lg="(!isMigrateFromVmware && showManagedInstances) ? 12 : 24">
<a-card class="instances-card"> <a-card class="instances-card">
@ -493,6 +496,16 @@
</a-card> </a-card>
</a-col> </a-col>
</a-row> </a-row>
</a-tab-pane>
<a-tab-pane :key=2 tab="Import VM Tasks" v-if="isMigrateFromVmware">
<ImportVmTasks
:tasks="importVmTasks"
:loading="loadingImportVmTasks"
@fetch-import-vm-tasks="fetchImportVmTasks"
/>
</a-tab-pane>
</a-tabs>
</a-card> </a-card>
<a-modal <a-modal
@ -546,6 +559,7 @@ import ImportUnmanagedInstances from '@/views/tools/ImportUnmanagedInstance'
import ResourceIcon from '@/components/view/ResourceIcon' import ResourceIcon from '@/components/view/ResourceIcon'
import SelectVmwareVcenter from '@/views/tools/SelectVmwareVcenter' import SelectVmwareVcenter from '@/views/tools/SelectVmwareVcenter'
import TooltipLabel from '@/components/widgets/TooltipLabel.vue' import TooltipLabel from '@/components/widgets/TooltipLabel.vue'
import ImportVmTasks from '@/views/tools/ImportVmTasks.vue'
export default { export default {
components: { components: {
@ -555,7 +569,8 @@ export default {
SearchView, SearchView,
ImportUnmanagedInstances, ImportUnmanagedInstances,
ResourceIcon, ResourceIcon,
SelectVmwareVcenter SelectVmwareVcenter,
ImportVmTasks
}, },
name: 'ManageVms', name: 'ManageVms',
data () { data () {
@ -747,7 +762,10 @@ export default {
selectedUnmanagedInstance: {}, selectedUnmanagedInstance: {},
query: {}, query: {},
vmwareVcenterType: undefined, vmwareVcenterType: undefined,
selectedVmwareVcenter: undefined selectedVmwareVcenter: undefined,
activeTabKey: 1,
loadingImportVmTasks: false,
importVmTasks: []
} }
}, },
created () { created () {
@ -1084,6 +1102,7 @@ export default {
this.page.managed = 1 this.page.managed = 1
this.managedInstances = [] this.managedInstances = []
this.managedInstancesSelectedRowKeys = [] this.managedInstancesSelectedRowKeys = []
this.activeTabKey = 1
}, },
onSelectHypervisor (value) { onSelectHypervisor (value) {
this.sourceHypervisor = value this.sourceHypervisor = value
@ -1154,6 +1173,27 @@ export default {
this.updateQuery('scope', value) this.updateQuery('scope', value)
this.fetchOptions(this.params.pools, 'pools', value) this.fetchOptions(this.params.pools, 'pools', value)
}, },
onTabChange (e) {
if (e === 2) {
this.fetchImportVmTasks()
}
},
fetchImportVmTasks (filter) {
this.loadingImportVmTasks = true
const params = {
zoneid: this.zoneId
}
if (filter && filter === 'completed') {
params.showcompleted = true
}
getAPI('listImportVmTasks', params).then(response => {
this.importVmTasks = response.listimportvmtasksresponse.importvmtask || []
}).catch(error => {
this.$notifyError(error)
}).finally(() => {
this.loadingImportVmTasks = false
})
},
fetchInstances () { fetchInstances () {
this.fetchUnmanagedInstances() this.fetchUnmanagedInstances()
if (this.isUnmanaged) { if (this.isUnmanaged) {

View File

@ -244,6 +244,7 @@ public class Script implements Callable<String> {
try { try {
_logger.trace(String.format("Creating process for command [%s].", commandLine)); _logger.trace(String.format("Creating process for command [%s].", commandLine));
ProcessBuilder pb = new ProcessBuilder(command); ProcessBuilder pb = new ProcessBuilder(command);
pb.redirectErrorStream(true); pb.redirectErrorStream(true);
if (_workDir != null) if (_workDir != null)