From d7c71013fa04ea93cfac89994bd6ed8e3c5744e5 Mon Sep 17 00:00:00 2001 From: kishankavala Date: Thu, 14 Dec 2023 13:08:56 +0530 Subject: [PATCH] KVM Ingestion - Import Instance (#7976) This PR adds new functionality to import KVM instances from an external host or from disk images in local or shared storage. Doc PR: https://github.com/apache/cloudstack-documentation/pull/356 --- .../main/java/com/cloud/vm/UserVmService.java | 4 +- .../java/com/cloud/vm/VmDetailConstants.java | 2 + .../apache/cloudstack/api/ApiConstants.java | 3 + .../admin/vm/ImportUnmanagedInstanceCmd.java | 2 +- .../api/command/admin/vm/ImportVmCmd.java | 134 ++- .../command/admin/vm/ListVmsForImportCmd.java | 134 +++ .../cloudstack/vm/UnmanagedInstanceTO.java | 20 + .../cloudstack/vm/UnmanagedVMsManager.java | 7 + .../apache/cloudstack/vm/VmImportService.java | 4 + .../cloud/agent/api/CheckVolumeAnswer.java | 40 + .../cloud/agent/api/CheckVolumeCommand.java | 59 ++ .../agent/api/CopyRemoteVolumeAnswer.java | 61 ++ .../agent/api/CopyRemoteVolumeCommand.java | 101 ++ .../cloud/agent/api/GetRemoteVmsAnswer.java | 75 ++ .../cloud/agent/api/GetRemoteVmsCommand.java | 70 ++ .../api/GetUnmanagedInstancesAnswer.java | 4 + .../service/VolumeOrchestrationService.java | 3 + .../orchestration/VolumeOrchestrator.java | 45 + .../java/com/cloud/storage/dao/VolumeDao.java | 2 + .../com/cloud/storage/dao/VolumeDaoImpl.java | 14 + .../resource/LibvirtComputingResource.java | 57 +- .../kvm/resource/LibvirtDomainXMLParser.java | 106 +- .../hypervisor/kvm/resource/LibvirtVMDef.java | 18 +- .../LibvirtCheckVolumeCommandWrapper.java | 86 ++ ...LibvirtCopyRemoteVolumeCommandWrapper.java | 93 ++ .../LibvirtGetRemoteVmsCommandWrapper.java | 194 ++++ ...rtGetUnmanagedInstancesCommandWrapper.java | 227 +++++ ...epareUnmanageVMInstanceCommandWrapper.java | 51 + .../com/cloud/api/query/QueryManagerImpl.java | 31 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 22 +- .../vm/UnmanagedVMsManagerImpl.java | 938 ++++++++++++++++-- .../vm/UnmanagedVMsManagerImplTest.java | 141 ++- tools/apidoc/gen_toc.py | 4 +- ui/public/locales/en.json | 27 +- ui/src/config/section/compute.js | 2 +- .../wizard/ComputeOfferingSelection.vue | 2 +- .../compute/wizard/MultiDiskSelection.vue | 6 +- .../views/tools/ImportUnmanagedInstance.vue | 266 ++++- ui/src/views/tools/ManageInstances.vue | 747 +++++++++++--- .../java/com/cloud/utils/ssh/SshHelper.java | 21 +- 40 files changed, 3467 insertions(+), 356 deletions(-) create mode 100644 api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmsForImportCmd.java create mode 100644 core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/CheckVolumeCommand.java create mode 100644 core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeCommand.java create mode 100644 core/src/main/java/com/cloud/agent/api/GetRemoteVmsAnswer.java create mode 100644 core/src/main/java/com/cloud/agent/api/GetRemoteVmsCommand.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetUnmanagedInstancesCommandWrapper.java create mode 100644 plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareUnmanageVMInstanceCommandWrapper.java diff --git a/api/src/main/java/com/cloud/vm/UserVmService.java b/api/src/main/java/com/cloud/vm/UserVmService.java index 94b120aff249..702197b73ac7 100644 --- a/api/src/main/java/com/cloud/vm/UserVmService.java +++ b/api/src/main/java/com/cloud/vm/UserVmService.java @@ -16,6 +16,7 @@ // under the License. package com.cloud.vm; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -538,7 +539,8 @@ UserVm upgradeVirtualMachine(ScaleVMCmd cmd) throws ResourceUnavailableException UserVm importVM(final DataCenter zone, final Host host, final VirtualMachineTemplate template, final String instanceName, final String displayName, final Account owner, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOffering serviceOffering, final String sshPublicKey, - final String hostName, final HypervisorType hypervisorType, final Map customParameters, final VirtualMachine.PowerState powerState) throws InsufficientCapacityException; + final String hostName, final HypervisorType hypervisorType, final Map customParameters, + final VirtualMachine.PowerState powerState, final LinkedHashMap> networkNicMap) throws InsufficientCapacityException; /** * Unmanage a guest VM from CloudStack diff --git a/api/src/main/java/com/cloud/vm/VmDetailConstants.java b/api/src/main/java/com/cloud/vm/VmDetailConstants.java index dc3045debebf..ec5ec48a57cf 100644 --- a/api/src/main/java/com/cloud/vm/VmDetailConstants.java +++ b/api/src/main/java/com/cloud/vm/VmDetailConstants.java @@ -40,6 +40,8 @@ public interface VmDetailConstants { String KVM_VNC_PORT = "kvm.vnc.port"; String KVM_VNC_ADDRESS = "kvm.vnc.address"; String TPM_VERSION = "tpmversion"; + String KVM_VNC_PASSWORD = "kvm.vnc.password"; + // KVM specific, custom virtual GPU hardware String VIDEO_HARDWARE = "video.hardware"; String VIDEO_RAM = "video.ram"; diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 606913c70d87..99f66e5a85a0 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -212,6 +212,7 @@ public class ApiConstants { public static final String HOST_IDS = "hostids"; public static final String HOST_IP = "hostip"; public static final String HOST_NAME = "hostname"; + public static final String HOST = "host"; public static final String HOST_CONTROL_STATE = "hostcontrolstate"; public static final String HOSTS_MAP = "hostsmap"; public static final String HYPERVISOR = "hypervisor"; @@ -1124,7 +1125,9 @@ public class ApiConstants { public static final String SOURCE_NAT_IP = "sourcenatipaddress"; public static final String SOURCE_NAT_IP_ID = "sourcenatipaddressid"; public static final String HAS_RULES = "hasrules"; + public static final String DISK_PATH = "diskpath"; public static final String IMPORT_SOURCE = "importsource"; + public static final String TEMP_PATH = "temppath"; public static final String OBJECT_STORAGE = "objectstore"; public static final String FIRST_LOGIN = "firstlogin"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java index 532a3f0d3929..d632c786a16a 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportUnmanagedInstanceCmd.java @@ -84,7 +84,7 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd { @Parameter(name = ApiConstants.NAME, type = CommandType.STRING, required = true, - description = "the hypervisor name of the instance") + description = "the name of the instance as it is known to the hypervisor") private String name; @Parameter(name = ApiConstants.DISPLAY_NAME, diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java index 01f517fb8375..e8b9f3addde7 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ImportVmCmd.java @@ -31,13 +31,18 @@ import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.HostResponse; +import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.api.response.VmwareDatacenterResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.vm.VmImportService; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.apache.log4j.Logger; +import javax.inject.Inject; + @APICommand(name = "importVm", description = "Import virtual machine from a unmanaged host into CloudStack", responseObject = UserVmResponse.class, @@ -47,21 +52,72 @@ authorized = {RoleType.Admin}, since = "4.19.0") public class ImportVmCmd extends ImportUnmanagedInstanceCmd { - public static final Logger LOGGER = Logger.getLogger(ImportVmCmd.class); + @Inject + public VmImportService vmImportService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.UUID, + entityType = ZoneResponse.class, + required = true, + description = "the zone ID") + private Long zoneId; + + @Parameter(name = ApiConstants.USERNAME, + type = CommandType.STRING, + description = "the username for the host") + private String username; + + @Parameter(name = ApiConstants.PASSWORD, + type = CommandType.STRING, + description = "the password for the host") + private String password; + + @Parameter(name = ApiConstants.HOST, + type = CommandType.STRING, + description = "the host name or IP address") + private String host; + @Parameter(name = ApiConstants.HYPERVISOR, type = CommandType.STRING, required = true, description = "hypervisor type of the host") private String hypervisor; + @Parameter(name = ApiConstants.DISK_PATH, + type = CommandType.STRING, + description = "path of the disk image") + private String diskPath; + @Parameter(name = ApiConstants.IMPORT_SOURCE, type = CommandType.STRING, required = true, description = "Source location for Import" ) private String importSource; + @Parameter(name = ApiConstants.NETWORK_ID, + type = CommandType.UUID, + entityType = NetworkResponse.class, + description = "the network ID") + private Long networkId; + + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "Host where local disk is located") + private Long hostId; + + @Parameter(name = ApiConstants.STORAGE_ID, type = CommandType.UUID, entityType = StoragePoolResponse.class, description = "Shared storage pool where disk is located") + private Long storagePoolId; + + @Parameter(name = ApiConstants.TEMP_PATH, + type = CommandType.STRING, + description = "Temp Path on external host for disk image copy" ) + private String tmpPath; + // Import from Vmware to KVM migration parameters @Parameter(name = ApiConstants.EXISTING_VCENTER_ID, @@ -73,7 +129,7 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { @Parameter(name = ApiConstants.HOST_IP, type = BaseCmd.CommandType.STRING, description = "(only for importing migrated VMs from Vmware to KVM) VMware ESXi host IP/Name.") - private String host; + private String hostip; @Parameter(name = ApiConstants.VCENTER, type = CommandType.STRING, @@ -88,14 +144,6 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { description = "(only for importing migrated VMs from Vmware to KVM) Name of VMware cluster.") private String clusterName; - @Parameter(name = ApiConstants.USERNAME, type = CommandType.STRING, - description = "(only for importing migrated VMs from Vmware to KVM) The Username required to connect to resource.") - private String username; - - @Parameter(name = ApiConstants.PASSWORD, type = CommandType.STRING, - description = "(only for importing migrated VMs from Vmware to KVM) The password for the specified username.") - private String password; - @Parameter(name = ApiConstants.CONVERT_INSTANCE_HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "(only for importing migrated VMs from Vmware to KVM) optional - the host to perform the virt-v2v migration from VMware to KVM.") private Long convertInstanceHostId; @@ -104,30 +152,20 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { description = "(only for importing migrated VMs from Vmware to KVM) optional - the temporary storage pool to perform the virt-v2v migration from VMware to KVM.") private Long convertStoragePoolId; - @Override - public String getEventType() { - return EventTypes.EVENT_VM_IMPORT; - } + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// - @Override - public String getEventDescription() { - String vmName = getName(); - if (ObjectUtils.anyNotNull(vcenter, existingVcenterId)) { - String msg = StringUtils.isNotBlank(vcenter) ? - String.format("external vCenter: %s - datacenter: %s", vcenter, datacenterName) : - String.format("existing vCenter Datacenter with ID: %s", existingVcenterId); - return String.format("Importing unmanaged VM: %s from %s - VM: %s", getDisplayName(), msg, vmName); - } - return String.format("Importing unmanaged VM: %s", vmName); + public Long getZoneId() { + return zoneId; } - public Long getExistingVcenterId() { return existingVcenterId; } - public String getHost() { - return host; + public String getHostIp() { + return hostip; } public String getVcenter() { @@ -150,6 +188,10 @@ public String getPassword() { return password; } + public String getHost() { + return host; + } + public Long getConvertInstanceHostId() { return convertInstanceHostId; } @@ -162,10 +204,47 @@ public String getHypervisor() { return hypervisor; } + public String getDiskPath() { + return diskPath; + } + public String getImportSource() { return importSource; } + public Long getHostId() { + return hostId; + } + + public Long getStoragePoolId() { + return storagePoolId; + } + + public String getTmpPath() { + return tmpPath; + } + + public Long getNetworkId() { + return networkId; + } + + @Override + public String getEventType() { + return EventTypes.EVENT_VM_IMPORT; + } + + @Override + public String getEventDescription() { + String vmName = getName(); + if (ObjectUtils.anyNotNull(vcenter, existingVcenterId)) { + String msg = StringUtils.isNotBlank(vcenter) ? + String.format("external vCenter: %s - datacenter: %s", vcenter, datacenterName) : + String.format("existing vCenter Datacenter with ID: %s", existingVcenterId); + return String.format("Importing unmanaged VM: %s from %s - VM: %s", getDisplayName(), msg, vmName); + } + return String.format("Importing unmanaged VM: %s", vmName); + } + ///////////////////////////////////////////////////// /////////////// API Implementation/////////////////// ///////////////////////////////////////////////////// @@ -176,5 +255,4 @@ public void execute() throws ResourceUnavailableException, InsufficientCapacityE response.setResponseName(getCommandName()); setResponseObject(response); } - } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmsForImportCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmsForImportCmd.java new file mode 100644 index 000000000000..88df04d9ef56 --- /dev/null +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vm/ListVmsForImportCmd.java @@ -0,0 +1,134 @@ +// 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.ListResponse; +import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; +import org.apache.cloudstack.api.response.ZoneResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.cloudstack.vm.VmImportService; +import org.apache.log4j.Logger; + +import javax.inject.Inject; + +@APICommand(name = "listVmsForImport", + description = "Lists virtual machines on a unmanaged host", + responseObject = UnmanagedInstanceResponse.class, + responseView = ResponseObject.ResponseView.Full, + entityType = {UnmanagedInstanceTO.class}, + requestHasSensitiveInfo = false, + responseHasSensitiveInfo = true, + authorized = {RoleType.Admin}, + since = "4.19.0") +public class ListVmsForImportCmd extends BaseListCmd { + public static final Logger LOGGER = Logger.getLogger(ListVmsForImportCmd.class.getName()); + + @Inject + public VmImportService vmImportService; + + ///////////////////////////////////////////////////// + //////////////// API parameters ///////////////////// + ///////////////////////////////////////////////////// + + @Parameter(name = ApiConstants.ZONE_ID, + type = CommandType.UUID, + entityType = ZoneResponse.class, + required = true, + description = "the zone ID") + private Long zoneId; + + @Parameter(name = ApiConstants.USERNAME, + type = CommandType.STRING, + description = "the username for the host") + private String username; + + @Parameter(name = ApiConstants.PASSWORD, + type = CommandType.STRING, + description = "the password for the host") + private String password; + + @Parameter(name = ApiConstants.HOST, + type = CommandType.STRING, + required = true, + description = "the host name or IP address") + private String host; + + @Parameter(name = ApiConstants.HYPERVISOR, + type = CommandType.STRING, + required = true, + description = "hypervisor type of the host") + private String hypervisor; + + ///////////////////////////////////////////////////// + /////////////////// Accessors /////////////////////// + ///////////////////////////////////////////////////// + + public Long getZoneId() { + return zoneId; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public String getHost() { + return host; + } + + public String getHypervisor() { + return hypervisor; + } + + ///////////////////////////////////////////////////// + /////////////// API Implementation/////////////////// + ///////////////////////////////////////////////////// + + @Override + public void execute() throws ResourceUnavailableException, InsufficientCapacityException, ServerApiException, ConcurrentOperationException, ResourceAllocationException, NetworkRuleConflictException { + ListResponse response = vmImportService.listVmsForImport(this); + response.setResponseName(getCommandName()); + setResponseObject(response); + } + + @Override + public long getEntityOwnerId() { + Account account = CallContext.current().getCallingAccount(); + if (account != null) { + return account.getId(); + } + return Account.ACCOUNT_ID_SYSTEM; + } +} diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java index a4748155b761..23e0e371714b 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedInstanceTO.java @@ -55,6 +55,8 @@ public enum PowerState { private List nics; + private String vncPassword; + public String getName() { return name; } @@ -167,6 +169,14 @@ public void setNics(List nics) { this.nics = nics; } + public String getVncPassword() { + return vncPassword; + } + + public void setVncPassword(String vncPassword) { + this.vncPassword = vncPassword; + } + public static class Disk { private String diskId; @@ -192,6 +202,8 @@ public static class Disk { private String datastorePath; + private int datastorePort; + private String datastoreType; public String getDiskId() { @@ -297,6 +309,14 @@ public String getDatastoreType() { public void setDatastoreType(String datastoreType) { this.datastoreType = datastoreType; } + + public void setDatastorePort(int datastorePort) { + this.datastorePort = datastorePort; + } + + public int getDatastorePort() { + return datastorePort; + } } public static class Nic { diff --git a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java index 2876a0127be5..53aece949649 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java +++ b/api/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManager.java @@ -17,13 +17,20 @@ package org.apache.cloudstack.vm; +import com.cloud.hypervisor.Hypervisor; import com.cloud.utils.component.PluggableService; import org.apache.cloudstack.framework.config.ConfigKey; import org.apache.cloudstack.framework.config.Configurable; +import static com.cloud.hypervisor.Hypervisor.HypervisorType.KVM; +import static com.cloud.hypervisor.Hypervisor.HypervisorType.VMware; public interface UnmanagedVMsManager extends VmImportService, UnmanageVMService, PluggableService, Configurable { ConfigKey UnmanageVMPreserveNic = new ConfigKey<>("Advanced", Boolean.class, "unmanage.vm.preserve.nics", "false", "If set to true, do not remove VM nics (and its MAC addresses) when unmanaging a VM, leaving them allocated but not reserved. " + "If set to false, nics are removed and MAC addresses can be reassigned", true, ConfigKey.Scope.Zone); + + static boolean isSupported(Hypervisor.HypervisorType hypervisorType) { + return hypervisorType == VMware || hypervisorType == KVM; + } } diff --git a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java index e5b121cd2d61..04ef248fb8ac 100644 --- a/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java +++ b/api/src/main/java/org/apache/cloudstack/vm/VmImportService.java @@ -20,6 +20,7 @@ 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.ListUnmanagedInstancesCmd; +import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; import org.apache.cloudstack.api.response.UserVmResponse; @@ -37,5 +38,8 @@ public String toString() { ListResponse listUnmanagedInstances(ListUnmanagedInstancesCmd cmd); UserVmResponse importUnmanagedInstance(ImportUnmanagedInstanceCmd cmd); + UserVmResponse importVm(ImportVmCmd cmd); + + ListResponse listVmsForImport(ListVmsForImportCmd cmd); } diff --git a/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java new file mode 100644 index 000000000000..dd136d8642f6 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CheckVolumeAnswer.java @@ -0,0 +1,40 @@ +// 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.agent.api; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class CheckVolumeAnswer extends Answer { + + private long size; + + CheckVolumeAnswer() { + } + + public CheckVolumeAnswer(CheckVolumeCommand cmd, String details, long size) { + super(cmd, true, details); + this.size = size; + } + + public long getSize() { + return size; + } + + public String getString() { + return "CheckVolumeAnswer [size=" + size + "]"; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/CheckVolumeCommand.java b/core/src/main/java/com/cloud/agent/api/CheckVolumeCommand.java new file mode 100644 index 000000000000..b4036bebf3ac --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CheckVolumeCommand.java @@ -0,0 +1,59 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.StorageFilerTO; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class CheckVolumeCommand extends Command { + + String srcFile; + + StorageFilerTO storageFilerTO; + + + public String getSrcFile() { + return srcFile; + } + + public void setSrcFile(String srcFile) { + this.srcFile = srcFile; + } + + public CheckVolumeCommand() { + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getString() { + return "CheckVolumeCommand [srcFile=" + srcFile + "]"; + } + + public StorageFilerTO getStorageFilerTO() { + return storageFilerTO; + } + + public void setStorageFilerTO(StorageFilerTO storageFilerTO) { + this.storageFilerTO = storageFilerTO; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java new file mode 100644 index 000000000000..f6d7cab45964 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeAnswer.java @@ -0,0 +1,61 @@ +// 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.agent.api; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class CopyRemoteVolumeAnswer extends Answer { + + private String remoteIp; + private String filename; + + private long size; + + CopyRemoteVolumeAnswer() { + } + + public CopyRemoteVolumeAnswer(CopyRemoteVolumeCommand cmd, String details, String filename, long size) { + super(cmd, true, details); + this.remoteIp = cmd.getRemoteIp(); + this.filename = filename; + this.size = size; + } + + public String getRemoteIp() { + return remoteIp; + } + + public void setRemoteIp(String remoteIp) { + this.remoteIp = remoteIp; + } + + public void setFilename(String filename) { + this.filename = filename; + } + + public String getFilename() { + return filename; + } + + public long getSize() { + return size; + } + + public String getString() { + return "CopyRemoteVolumeAnswer [remoteIp=" + remoteIp + "]"; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeCommand.java b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeCommand.java new file mode 100644 index 000000000000..82bc4d7cb459 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/CopyRemoteVolumeCommand.java @@ -0,0 +1,101 @@ +// +// 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.agent.api; + +import com.cloud.agent.api.to.StorageFilerTO; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class CopyRemoteVolumeCommand extends Command { + + String remoteIp; + String username; + String password; + String srcFile; + + String tmpPath; + + StorageFilerTO storageFilerTO; + + public CopyRemoteVolumeCommand(String remoteIp, String username, String password) { + this.remoteIp = remoteIp; + this.username = username; + this.password = password; + } + + public String getRemoteIp() { + return remoteIp; + } + + public void setRemoteIp(String remoteIp) { + this.remoteIp = remoteIp; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getSrcFile() { + return srcFile; + } + + public void setSrcFile(String srcFile) { + this.srcFile = srcFile; + } + + public CopyRemoteVolumeCommand() { + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getString() { + return "CopyRemoteVolumeCommand [remoteIp=" + remoteIp + "]"; + } + + public void setTempPath(String tmpPath) { + this.tmpPath = tmpPath; + } + + public String getTmpPath() { + return tmpPath; + } + + public StorageFilerTO getStorageFilerTO() { + return storageFilerTO; + } + + public void setStorageFilerTO(StorageFilerTO storageFilerTO) { + this.storageFilerTO = storageFilerTO; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetRemoteVmsAnswer.java b/core/src/main/java/com/cloud/agent/api/GetRemoteVmsAnswer.java new file mode 100644 index 000000000000..8cd072f1da1d --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetRemoteVmsAnswer.java @@ -0,0 +1,75 @@ +// 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.agent.api; + +import org.apache.cloudstack.vm.UnmanagedInstanceTO; + +import java.util.HashMap; +import java.util.List; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class GetRemoteVmsAnswer extends Answer { + + private String remoteIp; + private HashMap unmanagedInstances; + + List vmNames; + + GetRemoteVmsAnswer() { + } + + public GetRemoteVmsAnswer(GetRemoteVmsCommand cmd, String details, HashMap unmanagedInstances) { + super(cmd, true, details); + this.remoteIp = cmd.getRemoteIp(); + this.unmanagedInstances = unmanagedInstances; + } + + public GetRemoteVmsAnswer(GetRemoteVmsCommand cmd, String details, List vmNames) { + super(cmd, true, details); + this.remoteIp = cmd.getRemoteIp(); + this.vmNames = vmNames; + } + + public String getRemoteIp() { + return remoteIp; + } + + public void setRemoteIp(String remoteIp) { + this.remoteIp = remoteIp; + } + + public HashMap getUnmanagedInstances() { + return unmanagedInstances; + } + + public void setUnmanagedInstances(HashMap unmanagedInstances) { + this.unmanagedInstances = unmanagedInstances; + } + + public List getVmNames() { + return vmNames; + } + + public void setVmNames(List vmNames) { + this.vmNames = vmNames; + } + + public String getString() { + return "GetRemoteVmsAnswer [remoteIp=" + remoteIp + "]"; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetRemoteVmsCommand.java b/core/src/main/java/com/cloud/agent/api/GetRemoteVmsCommand.java new file mode 100644 index 000000000000..5c71d12dbd08 --- /dev/null +++ b/core/src/main/java/com/cloud/agent/api/GetRemoteVmsCommand.java @@ -0,0 +1,70 @@ +// +// 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.agent.api; + +@LogLevel(LogLevel.Log4jLevel.Trace) +public class GetRemoteVmsCommand extends Command { + + String remoteIp; + String username; + String password; + + public GetRemoteVmsCommand(String remoteIp, String username, String password) { + this.remoteIp = remoteIp; + this.username = username; + this.password = password; + } + + public String getRemoteIp() { + return remoteIp; + } + + public void setRemoteIp(String remoteIp) { + this.remoteIp = remoteIp; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public GetRemoteVmsCommand() { + } + + @Override + public boolean executeInSequence() { + return false; + } + + public String getString() { + return "GetRemoteVmsCommand [remoteIp=" + remoteIp + "]"; + } +} diff --git a/core/src/main/java/com/cloud/agent/api/GetUnmanagedInstancesAnswer.java b/core/src/main/java/com/cloud/agent/api/GetUnmanagedInstancesAnswer.java index 3c6118d426e6..771d472be2ae 100644 --- a/core/src/main/java/com/cloud/agent/api/GetUnmanagedInstancesAnswer.java +++ b/core/src/main/java/com/cloud/agent/api/GetUnmanagedInstancesAnswer.java @@ -30,6 +30,10 @@ public class GetUnmanagedInstancesAnswer extends Answer { GetUnmanagedInstancesAnswer() { } + public GetUnmanagedInstancesAnswer(GetUnmanagedInstancesCommand cmd, String details) { + super(cmd, false, details); + } + public GetUnmanagedInstancesAnswer(GetUnmanagedInstancesCommand cmd, String details, HashMap unmanagedInstances) { super(cmd, true, details); this.instanceName = cmd.getInstanceName(); diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java index 15f5b231be20..01123401faca 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/VolumeOrchestrationService.java @@ -168,6 +168,9 @@ List allocateTemplatedVolumes(Type type, String name, DiskOffering DiskProfile importVolume(Type type, String name, DiskOffering offering, Long size, Long minIops, Long maxIops, VirtualMachine vm, VirtualMachineTemplate template, Account owner, Long deviceId, Long poolId, String path, String chainInfo); + DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template, + Long deviceId, Long poolId, String path, String chainInfo, DiskProfile diskProfile); + /** * Unmanage VM volumes */ diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java index 55e0dfbec961..4433fbacb71d 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/VolumeOrchestrator.java @@ -2225,6 +2225,51 @@ public DiskProfile importVolume(Type type, String name, DiskOffering offering, L return toDiskProfile(vol, offering); } + @Override + public DiskProfile updateImportedVolume(Type type, DiskOffering offering, VirtualMachine vm, VirtualMachineTemplate template, + Long deviceId, Long poolId, String path, String chainInfo, DiskProfile diskProfile) { + + VolumeVO vol = _volsDao.findById(diskProfile.getVolumeId()); + if (vm != null) { + vol.setInstanceId(vm.getId()); + } + + if (deviceId != null) { + vol.setDeviceId(deviceId); + } else if (type.equals(Type.ROOT)) { + vol.setDeviceId(0l); + } else { + vol.setDeviceId(1l); + } + + if (template != null) { + if (ImageFormat.ISO.equals(template.getFormat())) { + vol.setIsoId(template.getId()); + } else if (Storage.TemplateType.DATADISK.equals(template.getTemplateType())) { + vol.setTemplateId(template.getId()); + } + if (type == Type.ROOT) { + vol.setTemplateId(template.getId()); + } + } + + // display flag matters only for the User vms + if (VirtualMachine.Type.User.equals(vm.getType())) { + UserVmVO userVm = _userVmDao.findById(vm.getId()); + vol.setDisplayVolume(userVm.isDisplayVm()); + } + + vol.setFormat(getSupportedImageFormatForCluster(vm.getHypervisorType())); + vol.setPoolId(poolId); + vol.setPath(path); + vol.setChainInfo(chainInfo); + vol.setSize(diskProfile.getSize()); + vol.setState(Volume.State.Ready); + vol.setAttached(new Date()); + _volsDao.update(vol.getId(), vol); + return toDiskProfile(vol, offering); + } + @Override public void unmanageVolumes(long vmId) { if (s_logger.isDebugEnabled()) { diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java index 6fa6658934c8..b6e786047aa7 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDao.java @@ -152,6 +152,8 @@ public interface VolumeDao extends GenericDao, StateDao listByPoolIdAndPaths(long id, List pathList); + VolumeVO findByPoolIdAndPath(long id, String path); + List listByIds(List ids); VolumeVO findByPath(String path); diff --git a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java index 602807e1881b..b8769cd9b987 100644 --- a/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/storage/dao/VolumeDaoImpl.java @@ -71,6 +71,7 @@ public class VolumeDaoImpl extends GenericDaoBase implements Vol protected GenericSearchBuilder primaryStorageSearch; protected GenericSearchBuilder primaryStorageSearch2; protected GenericSearchBuilder secondaryStorageSearch; + private final SearchBuilder poolAndPathSearch; @Inject ResourceTagDao _tagsDao; @@ -487,6 +488,11 @@ public VolumeDaoImpl() { volumeIdSearch.and("idIN", volumeIdSearch.entity().getId(), Op.IN); volumeIdSearch.done(); + poolAndPathSearch = createSearchBuilder(); + poolAndPathSearch.and("poolId", poolAndPathSearch.entity().getPoolId(), Op.EQ); + poolAndPathSearch.and("path", poolAndPathSearch.entity().getPath(), Op.EQ); + poolAndPathSearch.done(); + } @Override @@ -802,6 +808,14 @@ public List listByPoolIdAndPaths(long id, List pathList) { return listBy(sc); } + @Override + public VolumeVO findByPoolIdAndPath(long id, String path) { + SearchCriteria sc = poolAndPathSearch.create(); + sc.setParameters("poolId", id); + sc.setParameters("path", path); + return findOneBy(sc); + } + @Override public List listByIds(List ids) { if (CollectionUtils.isEmpty(ids)) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java index 9aa0b253fc5a..b0ea0923aaab 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtComputingResource.java @@ -2410,7 +2410,7 @@ private Pair, Integer> getMacAddressToNicNumPair(Connect co return new Pair, Integer>(macAddressToNicNum, devNum); } - protected PowerState convertToPowerState(final DomainState ps) { + public PowerState convertToPowerState(final DomainState ps) { final PowerState state = POWER_STATES_TABLE.get(ps); return state == null ? PowerState.PowerUnknown : state; } @@ -4018,7 +4018,39 @@ private String getIqn() { } } - protected List getAllVmNames(final Connect conn) { + /** + * Given a disk path on KVM host, attempts to find source host and path using mount command + * @param diskPath KVM host path for virtual disk + * @return Pair with IP of host and path + */ + public Pair getSourceHostPath(String diskPath) { + String sourceHostIp = null; + String sourcePath = null; + try { + String mountResult = Script.runSimpleBashScript("mount | grep \"" + diskPath + "\""); + s_logger.debug("Got mount result for " + diskPath + "\n\n" + mountResult); + if (StringUtils.isNotEmpty(mountResult)) { + String[] res = mountResult.strip().split(" "); + if (res[0].contains(":")) { + res = res[0].split(":"); + sourceHostIp = res[0].strip(); + sourcePath = res[1].strip(); + } else { + // Assume local storage + sourceHostIp = getPrivateIp(); + sourcePath = diskPath; + } + } + if (StringUtils.isNotEmpty(sourceHostIp) && StringUtils.isNotEmpty(sourcePath)) { + return new Pair<>(sourceHostIp, sourcePath); + } + } catch (Exception ex) { + s_logger.warn("Failed to list source host and IP for " + diskPath + ex.toString()); + } + return null; + } + + public List getAllVmNames(final Connect conn) { final ArrayList la = new ArrayList(); try { final String names[] = conn.listDefinedDomains(); @@ -5609,4 +5641,25 @@ public void setInterfaceDefQueueSettings(Map details, Integer cp } } } + + /* + Scp volume from remote host to local directory + */ + public String copyVolume(String srcIp, String username, String password, String localDir, String remoteFile, String tmpPath) { + try { + String outputFile = UUID.randomUUID().toString(); + StringBuilder command = new StringBuilder("qemu-img convert -O qcow2 "); + command.append(remoteFile); + command.append(" "+tmpPath); + command.append(outputFile); + s_logger.debug("Converting remoteFile: "+remoteFile); + SshHelper.sshExecute(srcIp, 22, username, null, password, command.toString()); + s_logger.debug("Copying remoteFile to: "+localDir); + SshHelper.scpFrom(srcIp, 22, username, null, password, localDir, tmpPath+outputFile); + s_logger.debug("Successfully copyied remoteFile to: "+localDir+"/"+outputFile); + return outputFile; + } catch (Exception e) { + throw new RuntimeException(e); + } + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java index a5565c2de34e..f165796adef2 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParser.java @@ -57,8 +57,10 @@ public class LibvirtDomainXMLParser { private final List channels = new ArrayList(); private final List watchDogDefs = new ArrayList(); private Integer vncPort; + private String vncPasswd; private String desc; - + private LibvirtVMDef.CpuTuneDef cpuTuneDef; + private LibvirtVMDef.CpuModeDef cpuModeDef; private String name; public boolean parseDomainXML(String domXML) { @@ -278,6 +280,14 @@ public boolean parseDomainXML(String domXML) { String name = getAttrValue("target", "name", channel); String state = getAttrValue("target", "state", channel); + if (ChannelDef.ChannelType.valueOf(type.toUpperCase()).equals(ChannelDef.ChannelType.SPICEVMC)) { + continue; + } + + if (path == null) { + path = ""; + } + ChannelDef def = null; if (StringUtils.isBlank(state)) { def = new ChannelDef(name, ChannelDef.ChannelType.valueOf(type.toUpperCase()), new File(path)); @@ -305,6 +315,12 @@ public boolean parseDomainXML(String domXML) { vncPort = null; } } + + String passwd = graphic.getAttribute("passwd"); + if (passwd != null) { + vncPasswd = passwd; + } + } NodeList rngs = devices.getElementsByTagName("rng"); @@ -317,6 +333,26 @@ public boolean parseDomainXML(String domXML) { String period = getAttrValue("rate", "period", rng); if (StringUtils.isAnyEmpty(bytes, period)) { s_logger.debug(String.format("Bytes and period in the rng section should not be null, please check the VM %s", name)); + } + + if (bytes == null) { + bytes = "0"; + } + + if (period == null) { + period = "0"; + } + + if (bytes == null) { + bytes = "0"; + } + + if (period == null) { + period = "0"; + } + + if (StringUtils.isEmpty(backendModel)) { + def = new RngDef(path, Integer.parseInt(bytes), Integer.parseInt(period)); } else { if (StringUtils.isEmpty(backendModel)) { def = new RngDef(path, Integer.parseInt(bytes), Integer.parseInt(period)); @@ -350,7 +386,8 @@ public boolean parseDomainXML(String domXML) { watchDogDefs.add(def); } - + extractCpuTuneDef(rootElement); + extractCpuModeDef(rootElement); return true; } catch (ParserConfigurationException e) { s_logger.debug(e.toString()); @@ -411,6 +448,10 @@ public List getInterfaces() { return interfaces; } + public String getVncPasswd() { + return vncPasswd; + } + public MemBalloonDef getMemBalloon() { return memBalloonDef; } @@ -438,4 +479,65 @@ public String getDescription() { public String getName() { return name; } + + public LibvirtVMDef.CpuTuneDef getCpuTuneDef() { + return cpuTuneDef; + } + + public LibvirtVMDef.CpuModeDef getCpuModeDef() { + return cpuModeDef; + } + + private void extractCpuTuneDef(final Element rootElement) { + NodeList cpuTunesList = rootElement.getElementsByTagName("cputune"); + if (cpuTunesList.getLength() > 0) { + cpuTuneDef = new LibvirtVMDef.CpuTuneDef(); + final Element cpuTuneDefElement = (Element) cpuTunesList.item(0); + final String cpuShares = getTagValue("shares", cpuTuneDefElement); + if (StringUtils.isNotBlank(cpuShares)) { + cpuTuneDef.setShares((Integer.parseInt(cpuShares))); + } + + final String quota = getTagValue("quota", cpuTuneDefElement); + if (StringUtils.isNotBlank(quota)) { + cpuTuneDef.setQuota((Integer.parseInt(quota))); + } + + final String period = getTagValue("period", cpuTuneDefElement); + if (StringUtils.isNotBlank(period)) { + cpuTuneDef.setPeriod((Integer.parseInt(period))); + } + } + } + + private void extractCpuModeDef(final Element rootElement){ + NodeList cpuModeList = rootElement.getElementsByTagName("cpu"); + if (cpuModeList.getLength() > 0){ + cpuModeDef = new LibvirtVMDef.CpuModeDef(); + final Element cpuModeDefElement = (Element) cpuModeList.item(0); + final String cpuModel = getTagValue("model", cpuModeDefElement); + if (StringUtils.isNotBlank(cpuModel)){ + cpuModeDef.setModel(cpuModel); + } + NodeList cpuFeatures = cpuModeDefElement.getElementsByTagName("features"); + if (cpuFeatures.getLength() > 0) { + final ArrayList features = new ArrayList<>(cpuFeatures.getLength()); + for (int i = 0; i < cpuFeatures.getLength(); i++) { + final Element feature = (Element)cpuFeatures.item(i); + final String policy = feature.getAttribute("policy"); + String featureName = feature.getAttribute("name"); + if ("disable".equals(policy)) { + featureName = "-" + featureName; + } + features.add(featureName); + } + cpuModeDef.setFeatures(features); + } + final String sockets = getAttrValue("topology", "sockets", cpuModeDefElement); + final String cores = getAttrValue("topology", "cores", cpuModeDefElement); + if (StringUtils.isNotBlank(sockets) && StringUtils.isNotBlank(cores)) { + cpuModeDef.setTopology(Integer.parseInt(cores), Integer.parseInt(sockets)); + } + } + } } diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java index 1dd965ba9a17..23c46c3e2914 100644 --- a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/LibvirtVMDef.java @@ -1110,6 +1110,18 @@ public void setSerial(String serial) { public LibvirtDiskEncryptDetails getLibvirtDiskEncryptDetails() { return this.encryptDetails; } + public String getSourceHost() { + return _sourceHost; + } + + public int getSourceHostPort() { + return _sourcePort; + } + + public String getSourcePath() { + return _sourcePath; + } + @Override public String toString() { StringBuilder diskBuilder = new StringBuilder(); @@ -1777,6 +1789,10 @@ public String toString() { modeBuilder.append(""); return modeBuilder.toString(); } + + public int getCoresPerSocket() { + return _coresPerSocket; + } } public static class SerialDef { @@ -1833,7 +1849,7 @@ public String toString() { public final static class ChannelDef { enum ChannelType { - UNIX("unix"), SERIAL("serial"); + UNIX("unix"), SERIAL("serial"), SPICEVMC("spicevmc"); String type; ChannelType(String type) { diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java new file mode 100644 index 000000000000..8b0a5aab4619 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCheckVolumeCommandWrapper.java @@ -0,0 +1,86 @@ +// +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckVolumeAnswer; +import com.cloud.agent.api.CheckVolumeCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.log4j.Logger; +import org.libvirt.LibvirtException; + +import java.util.Map; + +@ResourceWrapper(handles = CheckVolumeCommand.class) +public final class LibvirtCheckVolumeCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtCheckVolumeCommandWrapper.class); + + @Override + public Answer execute(final CheckVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { + String result = null; + String srcFile = command.getSrcFile(); + StorageFilerTO storageFilerTO = command.getStorageFilerTO(); + KVMStoragePoolManager poolMgr = libvirtComputingResource.getStoragePoolMgr(); + KVMStoragePool pool = poolMgr.getStoragePool(storageFilerTO.getType(), storageFilerTO.getUuid()); + + try { + if (storageFilerTO.getType() == Storage.StoragePoolType.Filesystem || + storageFilerTO.getType() == Storage.StoragePoolType.NetworkFilesystem) { + final KVMPhysicalDisk vol = pool.getPhysicalDisk(srcFile); + final String path = vol.getPath(); + long size = getVirtualSizeFromFile(path); + return new CheckVolumeAnswer(command, "", size); + } else { + return new Answer(command, false, "Unsupported Storage Pool"); + } + + } catch (final Exception e) { + s_logger.error("Error while locating disk: "+ e.getMessage()); + return new Answer(command, false, result); + } + } + + private long getVirtualSizeFromFile(String path) { + try { + QemuImg qemu = new QemuImg(0); + QemuImgFile qemuFile = new QemuImgFile(path); + Map info = qemu.info(qemuFile); + if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { + return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); + } else { + throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); + } + } catch (QemuImgException | LibvirtException ex) { + throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java new file mode 100644 index 000000000000..e48edd8eec0d --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtCopyRemoteVolumeCommandWrapper.java @@ -0,0 +1,93 @@ +// +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CopyRemoteVolumeAnswer; +import com.cloud.agent.api.CopyRemoteVolumeCommand; +import com.cloud.agent.api.to.StorageFilerTO; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.storage.KVMPhysicalDisk; +import com.cloud.hypervisor.kvm.storage.KVMStoragePool; +import com.cloud.hypervisor.kvm.storage.KVMStoragePoolManager; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.storage.Storage; +import com.cloud.utils.exception.CloudRuntimeException; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.log4j.Logger; +import org.libvirt.LibvirtException; + +import java.util.Map; + +@ResourceWrapper(handles = CopyRemoteVolumeCommand.class) +public final class LibvirtCopyRemoteVolumeCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtCopyRemoteVolumeCommandWrapper.class); + + @Override + public Answer execute(final CopyRemoteVolumeCommand command, final LibvirtComputingResource libvirtComputingResource) { + String result = null; + String srcIp = command.getRemoteIp(); + String username = command.getUsername(); + String password = command.getPassword(); + String srcFile = command.getSrcFile(); + StorageFilerTO storageFilerTO = command.getStorageFilerTO(); + String tmpPath = command.getTmpPath(); + KVMStoragePoolManager poolMgr = libvirtComputingResource.getStoragePoolMgr(); + KVMStoragePool pool = poolMgr.getStoragePool(storageFilerTO.getType(), storageFilerTO.getUuid()); + String dstPath = pool.getLocalPath(); + + try { + if (storageFilerTO.getType() == Storage.StoragePoolType.Filesystem || + storageFilerTO.getType() == Storage.StoragePoolType.NetworkFilesystem) { + String filename = libvirtComputingResource.copyVolume(srcIp, username, password, dstPath, srcFile, tmpPath); + s_logger.debug("Volume Copy Successful"); + final KVMPhysicalDisk vol = pool.getPhysicalDisk(filename); + final String path = vol.getPath(); + long size = getVirtualSizeFromFile(path); + return new CopyRemoteVolumeAnswer(command, "", filename, size); + } else { + return new Answer(command, false, "Unsupported Storage Pool"); + } + + } catch (final Exception e) { + s_logger.error("Error while copying file from remote host: "+ e.getMessage()); + return new Answer(command, false, result); + } + } + + private long getVirtualSizeFromFile(String path) { + try { + QemuImg qemu = new QemuImg(0); + QemuImgFile qemuFile = new QemuImgFile(path); + Map info = qemu.info(qemuFile); + if (info.containsKey(QemuImg.VIRTUAL_SIZE)) { + return Long.parseLong(info.get(QemuImg.VIRTUAL_SIZE)); + } else { + throw new CloudRuntimeException("Unable to determine virtual size of volume at path " + path); + } + } catch (QemuImgException | LibvirtException ex) { + throw new CloudRuntimeException("Error when inspecting volume at path " + path, ex); + } + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java new file mode 100644 index 000000000000..700f058b59b8 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetRemoteVmsCommandWrapper.java @@ -0,0 +1,194 @@ +// +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.Answer; +import com.cloud.agent.api.GetRemoteVmsAnswer; +import com.cloud.agent.api.GetRemoteVmsCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtConnection; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.DomainBlockInfo; +import org.libvirt.DomainInfo; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@ResourceWrapper(handles = GetRemoteVmsCommand.class) +public final class LibvirtGetRemoteVmsCommandWrapper extends CommandWrapper { + + private static final Logger s_logger = Logger.getLogger(LibvirtGetRemoteVmsCommandWrapper.class); + + @Override + public Answer execute(final GetRemoteVmsCommand command, final LibvirtComputingResource libvirtComputingResource) { + String result = null; + String hypervisorURI = "qemu+tcp://" + command.getRemoteIp() + + "/system"; + HashMap unmanagedInstances = new HashMap<>(); + try { + Connect conn = LibvirtConnection.getConnection(hypervisorURI); + final List allVmNames = libvirtComputingResource.getAllVmNames(conn); + for (String name : allVmNames) { + final Domain domain = libvirtComputingResource.getDomain(conn, name); + + final DomainInfo.DomainState ps = domain.getInfo().state; + + final VirtualMachine.PowerState state = libvirtComputingResource.convertToPowerState(ps); + + s_logger.debug("VM " + domain.getName() + ": powerstate = " + ps + "; vm state=" + state.toString()); + + if (state == VirtualMachine.PowerState.PowerOff) { + try { + UnmanagedInstanceTO instance = getUnmanagedInstance(libvirtComputingResource, domain, conn); + unmanagedInstances.put(instance.getName(), instance); + } catch (Exception e) { + s_logger.error("Error while fetching instance details", e); + } + } + domain.free(); + } + s_logger.debug("Found Vms: "+ unmanagedInstances.size()); + return new GetRemoteVmsAnswer(command, "", unmanagedInstances); + } catch (final LibvirtException e) { + s_logger.error("Error while listing stopped Vms on remote host: "+ e.getMessage()); + return new Answer(command, false, result); + } + } + + private UnmanagedInstanceTO getUnmanagedInstance(LibvirtComputingResource libvirtComputingResource, Domain domain, Connect conn) { + try { + final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + parser.parseDomainXML(domain.getXMLDesc(1)); + + final UnmanagedInstanceTO instance = new UnmanagedInstanceTO(); + instance.setName(domain.getName()); + if (parser.getCpuModeDef() != null) { + instance.setCpuCoresPerSocket(parser.getCpuModeDef().getCoresPerSocket()); + } + Long memory = domain.getMaxMemory(); + instance.setMemory(memory.intValue()/1024); + if (parser.getCpuTuneDef() !=null) { + instance.setCpuSpeed(parser.getCpuTuneDef().getShares()); + } + instance.setPowerState(getPowerState(libvirtComputingResource.getVmState(conn,domain.getName()))); + instance.setNics(getUnmanagedInstanceNics(parser.getInterfaces())); + instance.setDisks(getUnmanagedInstanceDisks(parser.getDisks(),libvirtComputingResource, domain)); + instance.setVncPassword(parser.getVncPasswd() + "aaaaaaaaaaaaaa"); // Suffix back extra characters for DB compatibility + + return instance; + } catch (Exception e) { + s_logger.debug("Unable to retrieve unmanaged instance info. ", e); + throw new CloudRuntimeException("Unable to retrieve unmanaged instance info. " + e.getMessage()); + } + } + + private UnmanagedInstanceTO.PowerState getPowerState(VirtualMachine.PowerState vmPowerState) { + switch (vmPowerState) { + case PowerOn: + return UnmanagedInstanceTO.PowerState.PowerOn; + case PowerOff: + return UnmanagedInstanceTO.PowerState.PowerOff; + default: + return UnmanagedInstanceTO.PowerState.PowerUnknown; + + } + } + + private List getUnmanagedInstanceNics(List interfaces) { + final ArrayList nics = new ArrayList<>(interfaces.size()); + int counter = 0; + for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { + final UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); + nic.setNicId(String.valueOf(counter++)); + nic.setMacAddress(interfaceDef.getMacAddress()); + nic.setAdapterType(interfaceDef.getModel().toString()); + nic.setNetwork(interfaceDef.getDevName()); + nic.setPciSlot(interfaceDef.getSlot().toString()); + nic.setVlan(interfaceDef.getVlanTag()); + nics.add(nic); + } + return nics; + } + + private List getUnmanagedInstanceDisks(List disksInfo, + LibvirtComputingResource libvirtComputingResource, + Domain dm){ + final ArrayList disks = new ArrayList<>(disksInfo.size()); + int counter = 0; + for (LibvirtVMDef.DiskDef diskDef : disksInfo) { + if (diskDef.getDeviceType() != LibvirtVMDef.DiskDef.DeviceType.DISK) { + continue; + } + + final UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); + + disk.setPosition(counter); + + Long size; + try { + DomainBlockInfo blockInfo = dm.blockInfo(diskDef.getSourcePath()); + size = blockInfo.getCapacity(); + } catch (LibvirtException e) { + throw new RuntimeException(e); + } + + disk.setCapacity(size); + disk.setDiskId(String.valueOf(counter++)); + disk.setLabel(diskDef.getDiskLabel()); + disk.setController(diskDef.getBusType().toString()); + + + Pair sourceHostPath = getSourceHostPath(libvirtComputingResource, diskDef.getSourcePath()); + if (sourceHostPath != null) { + disk.setDatastoreHost(sourceHostPath.first()); + disk.setDatastorePath(sourceHostPath.second()); + } else { + disk.setDatastorePath(diskDef.getSourcePath()); + disk.setDatastoreHost(diskDef.getSourceHost()); + } + + disk.setDatastoreType(diskDef.getDiskType().toString()); + disk.setDatastorePort(diskDef.getSourceHostPort()); + disks.add(disk); + } + return disks; + } + + private Pair getSourceHostPath(LibvirtComputingResource libvirtComputingResource, String diskPath) { + int pathEnd = diskPath.lastIndexOf("/"); + if (pathEnd >= 0) { + diskPath = diskPath.substring(0, pathEnd); + return libvirtComputingResource.getSourceHostPath(diskPath); + } + return null; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetUnmanagedInstancesCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetUnmanagedInstancesCommandWrapper.java new file mode 100644 index 000000000000..a2d84063d741 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtGetUnmanagedInstancesCommandWrapper.java @@ -0,0 +1,227 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.GetUnmanagedInstancesAnswer; +import com.cloud.agent.api.GetUnmanagedInstancesCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.hypervisor.kvm.resource.LibvirtDomainXMLParser; +import com.cloud.hypervisor.kvm.resource.LibvirtVMDef; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import com.cloud.utils.Pair; +import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.VirtualMachine; +import org.apache.cloudstack.utils.qemu.QemuImg; +import org.apache.cloudstack.utils.qemu.QemuImgException; +import org.apache.cloudstack.utils.qemu.QemuImgFile; +import org.apache.cloudstack.vm.UnmanagedInstanceTO; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; +import org.libvirt.LibvirtException; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ResourceWrapper(handles=GetUnmanagedInstancesCommand.class) +public final class LibvirtGetUnmanagedInstancesCommandWrapper extends CommandWrapper { + private static final Logger LOGGER = Logger.getLogger(LibvirtGetUnmanagedInstancesCommandWrapper.class); + + @Override + public GetUnmanagedInstancesAnswer execute(GetUnmanagedInstancesCommand command, LibvirtComputingResource libvirtComputingResource) { + LOGGER.info("Fetching unmanaged instance on host"); + + HashMap unmanagedInstances = new HashMap<>(); + try { + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + final Connect conn = libvirtUtilitiesHelper.getConnection(); + final List domains = getDomains(command, libvirtComputingResource, conn); + + for (Domain domain : domains) { + UnmanagedInstanceTO instance = getUnmanagedInstance(libvirtComputingResource, domain, conn); + if (instance != null) { + unmanagedInstances.put(instance.getName(), instance); + domain.free(); + } + } + } catch (Exception e) { + String err = String.format("Error listing unmanaged instances: %s", e.getMessage()); + LOGGER.error(err, e); + return new GetUnmanagedInstancesAnswer(command, err); + } + + return new GetUnmanagedInstancesAnswer(command, "OK", unmanagedInstances); + } + + private List getDomains(GetUnmanagedInstancesCommand command, + LibvirtComputingResource libvirtComputingResource, + Connect conn) throws LibvirtException, CloudRuntimeException { + final List domains = new ArrayList<>(); + final String vmNameCmd = command.getInstanceName(); + if (StringUtils.isNotBlank(vmNameCmd)) { + final Domain domain = libvirtComputingResource.getDomain(conn, vmNameCmd); + if (domain == null) { + String msg = String.format("VM %s not found", vmNameCmd); + LOGGER.error(msg); + throw new CloudRuntimeException(msg); + } + + checkIfVmExists(vmNameCmd,domain); + checkIfVmIsManaged(command,vmNameCmd,domain); + + domains.add(domain); + } else { + final List allVmNames = libvirtComputingResource.getAllVmNames(conn); + for (String name : allVmNames) { + if (!command.hasManagedInstance(name)) { + final Domain domain = libvirtComputingResource.getDomain(conn, name); + domains.add(domain); + } + } + } + return domains; + } + + private void checkIfVmExists(String vmNameCmd,final Domain domain) throws LibvirtException { + if (StringUtils.isNotEmpty(vmNameCmd) && + !vmNameCmd.equals(domain.getName())) { + LOGGER.error("GetUnmanagedInstancesCommand: exact vm name not found " + vmNameCmd); + throw new CloudRuntimeException("GetUnmanagedInstancesCommand: exact vm name not found " + vmNameCmd); + } + } + + private void checkIfVmIsManaged(GetUnmanagedInstancesCommand command,String vmNameCmd,final Domain domain) throws LibvirtException { + if (command.hasManagedInstance(domain.getName())) { + LOGGER.error("GetUnmanagedInstancesCommand: vm already managed " + vmNameCmd); + throw new CloudRuntimeException("GetUnmanagedInstancesCommand: vm already managed " + vmNameCmd); + } + } + private UnmanagedInstanceTO getUnmanagedInstance(LibvirtComputingResource libvirtComputingResource, Domain domain, Connect conn) { + try { + final LibvirtDomainXMLParser parser = new LibvirtDomainXMLParser(); + parser.parseDomainXML(domain.getXMLDesc(1)); + + final UnmanagedInstanceTO instance = new UnmanagedInstanceTO(); + instance.setName(domain.getName()); + + instance.setCpuCores((int) LibvirtComputingResource.countDomainRunningVcpus(domain)); + instance.setCpuSpeed(parser.getCpuTuneDef().getShares()/instance.getCpuCores()); + + if (parser.getCpuModeDef() != null) { + instance.setCpuCoresPerSocket(parser.getCpuModeDef().getCoresPerSocket()); + } + instance.setPowerState(getPowerState(libvirtComputingResource.getVmState(conn,domain.getName()))); + instance.setMemory((int) LibvirtComputingResource.getDomainMemory(domain) / 1024); + instance.setNics(getUnmanagedInstanceNics(parser.getInterfaces())); + instance.setDisks(getUnmanagedInstanceDisks(parser.getDisks(),libvirtComputingResource)); + instance.setVncPassword(parser.getVncPasswd() + "aaaaaaaaaaaaaa"); // Suffix back extra characters for DB compatibility + + return instance; + } catch (Exception e) { + LOGGER.info("Unable to retrieve unmanaged instance info. " + e.getMessage(), e); + return null; + } + } + + private UnmanagedInstanceTO.PowerState getPowerState(VirtualMachine.PowerState vmPowerState) { + switch (vmPowerState) { + case PowerOn: + return UnmanagedInstanceTO.PowerState.PowerOn; + case PowerOff: + return UnmanagedInstanceTO.PowerState.PowerOff; + default: + return UnmanagedInstanceTO.PowerState.PowerUnknown; + + } + } + + private List getUnmanagedInstanceNics(List interfaces) { + final ArrayList nics = new ArrayList<>(interfaces.size()); + int counter = 0; + for (LibvirtVMDef.InterfaceDef interfaceDef : interfaces) { + final UnmanagedInstanceTO.Nic nic = new UnmanagedInstanceTO.Nic(); + nic.setNicId(String.valueOf(counter++)); + nic.setMacAddress(interfaceDef.getMacAddress()); + nic.setAdapterType(interfaceDef.getModel().toString()); + nic.setNetwork(interfaceDef.getDevName()); + nic.setPciSlot(interfaceDef.getSlot().toString()); + nic.setVlan(interfaceDef.getVlanTag()); + nics.add(nic); + } + return nics; + } + + private List getUnmanagedInstanceDisks(List disksInfo, LibvirtComputingResource libvirtComputingResource){ + final ArrayList disks = new ArrayList<>(disksInfo.size()); + int counter = 0; + for (LibvirtVMDef.DiskDef diskDef : disksInfo) { + if (diskDef.getDeviceType() != LibvirtVMDef.DiskDef.DeviceType.DISK) { + continue; + } + + final UnmanagedInstanceTO.Disk disk = new UnmanagedInstanceTO.Disk(); + Long size = null; + String imagePath = null; + try { + QemuImgFile file = new QemuImgFile(diskDef.getSourcePath()); + QemuImg qemu = new QemuImg(0); + Map info = qemu.info(file); + size = Long.parseLong(info.getOrDefault("virtual_size", "0")); + imagePath = info.getOrDefault("image", null); + } catch (QemuImgException | LibvirtException e) { + throw new RuntimeException(e); + } + + disk.setPosition(counter); + disk.setCapacity(size); + disk.setDiskId(String.valueOf(counter++)); + disk.setLabel(diskDef.getDiskLabel()); + disk.setController(diskDef.getBusType().toString()); + + + Pair sourceHostPath = getSourceHostPath(libvirtComputingResource, diskDef.getSourcePath()); + if (sourceHostPath != null) { + disk.setDatastoreHost(sourceHostPath.first()); + disk.setDatastorePath(sourceHostPath.second()); + } else { + disk.setDatastorePath(diskDef.getSourcePath()); + disk.setDatastoreHost(diskDef.getSourceHost()); + } + + disk.setDatastoreType(diskDef.getDiskType().toString()); + disk.setDatastorePort(diskDef.getSourceHostPort()); + disk.setImagePath(imagePath); + disk.setDatastoreName(imagePath.substring(imagePath.lastIndexOf("/"))); + disks.add(disk); + } + return disks; + } + + private Pair getSourceHostPath(LibvirtComputingResource libvirtComputingResource, String diskPath) { + int pathEnd = diskPath.lastIndexOf("/"); + if (pathEnd >= 0) { + diskPath = diskPath.substring(0, pathEnd); + return libvirtComputingResource.getSourceHostPath(diskPath); + } + return null; + } +} diff --git a/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareUnmanageVMInstanceCommandWrapper.java b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareUnmanageVMInstanceCommandWrapper.java new file mode 100644 index 000000000000..683730890380 --- /dev/null +++ b/plugins/hypervisors/kvm/src/main/java/com/cloud/hypervisor/kvm/resource/wrapper/LibvirtPrepareUnmanageVMInstanceCommandWrapper.java @@ -0,0 +1,51 @@ +// 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.hypervisor.kvm.resource.wrapper; + +import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer; +import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand; +import com.cloud.hypervisor.kvm.resource.LibvirtComputingResource; +import com.cloud.resource.CommandWrapper; +import com.cloud.resource.ResourceWrapper; +import org.apache.log4j.Logger; +import org.libvirt.Connect; +import org.libvirt.Domain; + +@ResourceWrapper(handles=PrepareUnmanageVMInstanceCommand.class) +public final class LibvirtPrepareUnmanageVMInstanceCommandWrapper extends CommandWrapper { + private static final Logger LOGGER = Logger.getLogger(LibvirtPrepareUnmanageVMInstanceCommandWrapper.class); + @Override + public PrepareUnmanageVMInstanceAnswer execute(PrepareUnmanageVMInstanceCommand command, LibvirtComputingResource libvirtComputingResource) { + final String vmName = command.getInstanceName(); + final LibvirtUtilitiesHelper libvirtUtilitiesHelper = libvirtComputingResource.getLibvirtUtilitiesHelper(); + LOGGER.debug(String.format("Verify if KVM instance: [%s] is available before Unmanaging VM.", vmName)); + try { + final Connect conn = libvirtUtilitiesHelper.getConnectionByVmName(vmName); + final Domain domain = libvirtComputingResource.getDomain(conn, vmName); + if (domain == null) { + LOGGER.error("Prepare Unmanage VMInstanceCommand: vm not found " + vmName); + new PrepareUnmanageVMInstanceAnswer(command, false, String.format("Cannot find VM with name [%s] in KVM host.", vmName)); + } + } catch (Exception e){ + LOGGER.error("PrepareUnmanagedInstancesCommand failed due to " + e.getMessage()); + return new PrepareUnmanageVMInstanceAnswer(command, false, "Error: " + e.getMessage()); + } + + return new PrepareUnmanageVMInstanceAnswer(command, true, "OK"); + } +} diff --git a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java index b065af5293bb..b2b8340a2388 100644 --- a/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java +++ b/server/src/main/java/com/cloud/api/query/QueryManagerImpl.java @@ -34,7 +34,10 @@ import javax.inject.Inject; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.VMTemplateStoragePoolVO; +import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.host.Host; import com.cloud.host.dao.HostDao; @@ -572,6 +575,9 @@ public class QueryManagerImpl extends MutualExclusiveIdsManagerBase implements Q @Inject private PublicIpQuarantineDao publicIpQuarantineDao; + @Inject + private StoragePoolHostDao storagePoolHostDao; + private SearchCriteria getMinimumCpuServiceOfferingJoinSearchCriteria(int cpu) { SearchCriteria sc = _srvOfferingJoinDao.createSearchCriteria(); SearchCriteria sc1 = _srvOfferingJoinDao.createSearchCriteria(); @@ -2826,23 +2832,24 @@ private Pair, Integer> searchForAsyncJobsInternal(ListAsync @Override public ListResponse searchForStoragePools(ListStoragePoolsCmd cmd) { - Pair, Integer> result = cmd.getHostId() != null ? searchForLocalStorages(cmd) : searchForStoragePoolsInternal(cmd); + Pair, Integer> result = (ScopeType.HOST.name().equalsIgnoreCase(cmd.getScope()) && cmd.getHostId() != null) ? + searchForLocalStorages(cmd) : searchForStoragePoolsInternal(cmd); return createStoragesPoolResponse(result); } private Pair, Integer> searchForLocalStorages(ListStoragePoolsCmd cmd) { long id = cmd.getHostId(); - String scope = ScopeType.HOST.toString(); - Pair, Integer> localStorages; - - ListHostsCmd listHostsCmd = new ListHostsCmd(); - listHostsCmd.setId(id); - Pair, Integer> hosts = searchForServersInternal(listHostsCmd); - - cmd.setScope(scope); - localStorages = searchForStoragePoolsInternal(cmd); - - return localStorages; + List localstoragePools = storagePoolHostDao.listByHostId(id); + Long[] poolIds = new Long[localstoragePools.size()]; + int i = 0; + for(StoragePoolHostVO localstoragePool : localstoragePools) { + StoragePool storagePool = storagePoolDao.findById(localstoragePool.getPoolId()); + if (storagePool != null && storagePool.isLocal()) { + poolIds[i++] = localstoragePool.getPoolId(); + } + } + List pools = _poolJoinDao.searchByIds(poolIds); + return new Pair<>(pools, pools.size()); } private ListResponse createStoragesPoolResponse(Pair, Integer> storagePools) { diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 49050754cf35..c2ebff0ead76 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -132,6 +132,7 @@ import org.apache.cloudstack.utils.bytescale.ByteScaleUtils; import org.apache.cloudstack.utils.security.ParserUtils; import org.apache.cloudstack.vm.schedule.VMScheduleManager; +import org.apache.cloudstack.vm.UnmanagedVMsManager; import org.apache.commons.codec.binary.Base64; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; @@ -238,7 +239,6 @@ import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; -import com.cloud.hypervisor.Hypervisor; import com.cloud.hypervisor.Hypervisor.HypervisorType; import com.cloud.hypervisor.dao.HypervisorCapabilitiesDao; import com.cloud.hypervisor.kvm.dpdk.DpdkHelper; @@ -4512,6 +4512,8 @@ private UserVmVO commitUserVm(final boolean isImport, final DataCenter zone, fin setVmRequiredFieldsForImport(isImport, vm, zone, hypervisorType, host, lastHost, powerState); + setVncPasswordForKvmIfAvailable(customParameters, vm); + vm.setUserVmType(vmType); _vmDao.persist(vm); for (String key : customParameters.keySet()) { @@ -5146,7 +5148,6 @@ public UserVm startVirtualMachine(DeployVMCmd cmd) throws ResourceUnavailableExc Long hostId = cmd.getHostId(); Map additionalParams = new HashMap<>(); Map diskOfferingMap = cmd.getDataDiskTemplateToDiskOfferingMap(); - Map details = cmd.getDetails(); if (cmd instanceof DeployVMCmdByAdmin) { DeployVMCmdByAdmin adminCmd = (DeployVMCmdByAdmin)cmd; podId = adminCmd.getPodId(); @@ -8546,7 +8547,8 @@ private void destroyVolumeInContext(UserVmVO vm, boolean expunge, VolumeVO volum public UserVm importVM(final DataCenter zone, final Host host, final VirtualMachineTemplate template, final String instanceName, final String displayName, final Account owner, final String userData, final Account caller, final Boolean isDisplayVm, final String keyboard, final long accountId, final long userId, final ServiceOffering serviceOffering, final String sshPublicKeys, - final String hostName, final HypervisorType hypervisorType, final Map customParameters, final VirtualMachine.PowerState powerState) throws InsufficientCapacityException { + final String hostName, final HypervisorType hypervisorType, final Map customParameters, + final VirtualMachine.PowerState powerState, final LinkedHashMap> networkNicMap) throws InsufficientCapacityException { if (zone == null) { throw new InvalidParameterValueException("Unable to import virtual machine with invalid zone"); } @@ -8566,7 +8568,7 @@ public UserVm importVM(final DataCenter zone, final Host host, final VirtualMach final Boolean dynamicScalingEnabled = checkIfDynamicScalingCanBeEnabled(null, serviceOffering, template, zone.getId()); return commitUserVm(true, zone, host, lastHost, template, hostName, displayName, owner, null, null, userData, null, null, isDisplayVm, keyboard, - accountId, userId, serviceOffering, template.getFormat().equals(ImageFormat.ISO), sshPublicKeys, null, + accountId, userId, serviceOffering, template.getFormat().equals(ImageFormat.ISO), sshPublicKeys, networkNicMap, id, instanceName, uuidName, hypervisorType, customParameters, null, null, null, powerState, dynamicScalingEnabled, null, serviceOffering.getDiskOfferingId(), null); } @@ -8586,8 +8588,9 @@ public boolean unmanageUserVM(Long vmId) { return false; } - if (vm.getHypervisorType() != Hypervisor.HypervisorType.VMware) { - throw new UnsupportedServiceException("Unmanaging a VM is currently allowed for VMware VMs only"); + if (!UnmanagedVMsManager.isSupported(vm.getHypervisorType())) { + throw new UnsupportedServiceException("Unmanaging a VM is currently not supported on hypervisor " + + vm.getHypervisorType().toString()); } List volumes = _volsDao.findByInstance(vm.getId()); @@ -8854,4 +8857,11 @@ public UserVm removeVbmcToVM(RemoveVbmcToVMCmd cmd) { } return vm; } + + private void setVncPasswordForKvmIfAvailable(Map customParameters, UserVmVO vm){ + if (customParameters.containsKey(VmDetailConstants.KVM_VNC_PASSWORD) + && StringUtils.isNotEmpty(customParameters.get(VmDetailConstants.KVM_VNC_PASSWORD))) { + vm.setVncPassword(customParameters.get(VmDetailConstants.KVM_VNC_PASSWORD)); + } + } } diff --git a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java index 92079868e756..b7190f4ff217 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -17,114 +17,85 @@ package org.apache.cloudstack.vm; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.inject.Inject; - -import com.cloud.agent.api.ConvertInstanceAnswer; -import com.cloud.agent.api.ConvertInstanceCommand; -import com.cloud.agent.api.to.DataStoreTO; -import com.cloud.agent.api.to.RemoteInstanceTO; -import com.cloud.dc.VmwareDatacenterVO; -import com.cloud.dc.dao.VmwareDatacenterDao; -import com.cloud.event.ActionEventUtils; -import com.cloud.event.EventVO; -import com.cloud.exception.AgentUnavailableException; -import com.cloud.exception.OperationTimedoutException; -import com.cloud.host.Host; -import com.cloud.hypervisor.HypervisorGuru; -import com.cloud.hypervisor.HypervisorGuruManager; -import com.cloud.resource.ResourceState; -import com.cloud.storage.DataStoreRole; -import com.cloud.storage.ScopeType; -import com.cloud.storage.Storage; -import com.cloud.storage.StorageManager; -import com.cloud.storage.dao.StoragePoolHostDao; -import com.cloud.vm.VirtualMachineName; -import org.apache.cloudstack.api.ApiCommandResourceType; -import org.apache.cloudstack.api.ApiConstants; -import org.apache.cloudstack.api.ApiErrorCode; -import org.apache.cloudstack.api.ResponseGenerator; -import org.apache.cloudstack.api.ResponseObject; -import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; -import org.apache.cloudstack.api.command.admin.vm.ImportVmCmd; -import org.apache.cloudstack.api.command.admin.vm.ListUnmanagedInstancesCmd; -import org.apache.cloudstack.api.command.admin.vm.UnmanageVMInstanceCmd; -import org.apache.cloudstack.api.response.ListResponse; -import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; -import org.apache.cloudstack.api.response.UserVmResponse; -import org.apache.cloudstack.context.CallContext; -import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; -import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; -import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; -import org.apache.cloudstack.framework.config.ConfigKey; -import org.apache.cloudstack.framework.config.dao.ConfigurationDao; -import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; -import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; -import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; -import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; -import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.MapUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.log4j.Logger; - import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckVolumeAnswer; +import com.cloud.agent.api.CheckVolumeCommand; +import com.cloud.agent.api.ConvertInstanceAnswer; +import com.cloud.agent.api.ConvertInstanceCommand; +import com.cloud.agent.api.CopyRemoteVolumeAnswer; +import com.cloud.agent.api.CopyRemoteVolumeCommand; +import com.cloud.agent.api.GetRemoteVmsAnswer; +import com.cloud.agent.api.GetRemoteVmsCommand; import com.cloud.agent.api.GetUnmanagedInstancesAnswer; import com.cloud.agent.api.GetUnmanagedInstancesCommand; import com.cloud.agent.api.PrepareUnmanageVMInstanceAnswer; import com.cloud.agent.api.PrepareUnmanageVMInstanceCommand; +import com.cloud.agent.api.to.DataStoreTO; +import com.cloud.agent.api.to.RemoteInstanceTO; +import com.cloud.agent.api.to.StorageFilerTO; import com.cloud.configuration.Config; import com.cloud.configuration.Resource; import com.cloud.dc.DataCenter; +import com.cloud.dc.DataCenterVO; +import com.cloud.dc.VmwareDatacenterVO; import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; +import com.cloud.dc.dao.VmwareDatacenterDao; import com.cloud.deploy.DataCenterDeployment; import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeploymentPlanner; import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.event.ActionEvent; +import com.cloud.event.ActionEventUtils; import com.cloud.event.EventTypes; +import com.cloud.event.EventVO; import com.cloud.event.UsageEventUtils; +import com.cloud.exception.AgentUnavailableException; import com.cloud.exception.InsufficientAddressCapacityException; import com.cloud.exception.InsufficientCapacityException; import com.cloud.exception.InsufficientVirtualNetworkCapacityException; import com.cloud.exception.InvalidParameterValueException; +import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.PermissionDeniedException; import com.cloud.exception.ResourceAllocationException; import com.cloud.exception.UnsupportedServiceException; +import com.cloud.host.Host; import com.cloud.host.HostVO; import com.cloud.host.Status; import com.cloud.host.dao.HostDao; import com.cloud.hypervisor.Hypervisor; +import com.cloud.hypervisor.HypervisorGuru; +import com.cloud.hypervisor.HypervisorGuruManager; +import com.cloud.network.IpAddressManager; import com.cloud.network.Network; import com.cloud.network.NetworkModel; import com.cloud.network.Networks; +import com.cloud.network.PhysicalNetwork; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.network.dao.PhysicalNetworkDao; import com.cloud.offering.DiskOffering; +import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; +import com.cloud.offerings.NetworkOfferingVO; +import com.cloud.offerings.dao.NetworkOfferingDao; import com.cloud.org.Cluster; import com.cloud.resource.ResourceManager; +import com.cloud.resource.ResourceState; import com.cloud.serializer.GsonHelper; import com.cloud.server.ManagementService; import com.cloud.service.ServiceOfferingVO; import com.cloud.service.dao.ServiceOfferingDao; +import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.GuestOS; import com.cloud.storage.GuestOSHypervisor; +import com.cloud.storage.ScopeType; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; +import com.cloud.storage.Storage; +import com.cloud.storage.StorageManager; import com.cloud.storage.StoragePool; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VMTemplateVO; @@ -135,6 +106,7 @@ import com.cloud.storage.dao.GuestOSDao; import com.cloud.storage.dao.GuestOSHypervisorDao; import com.cloud.storage.dao.SnapshotDao; +import com.cloud.storage.dao.StoragePoolHostDao; import com.cloud.storage.dao.VMTemplateDao; import com.cloud.storage.dao.VMTemplatePoolDao; import com.cloud.storage.dao.VolumeDao; @@ -147,6 +119,7 @@ import com.cloud.uservm.UserVm; import com.cloud.utils.LogUtils; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.NetUtils; import com.cloud.vm.DiskProfile; @@ -157,16 +130,63 @@ import com.cloud.vm.VMInstanceVO; import com.cloud.vm.VirtualMachine; import com.cloud.vm.VirtualMachineManager; +import com.cloud.vm.VirtualMachineName; import com.cloud.vm.VirtualMachineProfile; import com.cloud.vm.VirtualMachineProfileImpl; import com.cloud.vm.VmDetailConstants; import com.cloud.vm.dao.NicDao; import com.cloud.vm.dao.UserVmDao; +import com.cloud.vm.dao.UserVmDetailsDao; import com.cloud.vm.dao.VMInstanceDao; import com.google.gson.Gson; +import org.apache.cloudstack.acl.ControlledEntity; +import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.api.ApiErrorCode; +import org.apache.cloudstack.api.ResponseGenerator; +import org.apache.cloudstack.api.ResponseObject; +import org.apache.cloudstack.api.ServerApiException; +import org.apache.cloudstack.api.command.admin.vm.ImportUnmanagedInstanceCmd; +import org.apache.cloudstack.api.command.admin.vm.ImportVmCmd; +import org.apache.cloudstack.api.command.admin.vm.ListUnmanagedInstancesCmd; +import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; +import org.apache.cloudstack.api.command.admin.vm.UnmanageVMInstanceCmd; +import org.apache.cloudstack.api.response.ListResponse; +import org.apache.cloudstack.api.response.NicResponse; +import org.apache.cloudstack.api.response.UnmanagedInstanceDiskResponse; +import org.apache.cloudstack.api.response.UnmanagedInstanceResponse; +import org.apache.cloudstack.api.response.UserVmResponse; +import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; +import org.apache.cloudstack.engine.orchestration.service.VolumeOrchestrationService; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStore; +import org.apache.cloudstack.engine.subsystem.api.storage.DataStoreManager; +import org.apache.cloudstack.framework.config.ConfigKey; +import org.apache.cloudstack.framework.config.dao.ConfigurationDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreDao; +import org.apache.cloudstack.storage.datastore.db.ImageStoreVO; +import org.apache.cloudstack.storage.datastore.db.PrimaryDataStoreDao; +import org.apache.cloudstack.storage.datastore.db.StoragePoolVO; +import org.apache.cloudstack.utils.volume.VirtualMachineDiskInfo; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.log4j.Logger; + +import javax.inject.Inject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; 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 KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME = "kvm-default-vm-import-dummy-template"; private static final Logger LOGGER = Logger.getLogger(UnmanagedVMsManagerImpl.class); private static final List importUnmanagedInstancesSupportedHypervisors = Arrays.asList(Hypervisor.HypervisorType.VMware, Hypervisor.HypervisorType.KVM); @@ -196,6 +216,8 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { @Inject private ResourceLimitService resourceLimitService; @Inject + private UserVmDetailsDao userVmDetailsDao; + @Inject private UserVmManager userVmManager; @Inject private ResponseGenerator responseGenerator; @@ -234,14 +256,24 @@ public class UnmanagedVMsManagerImpl implements UnmanagedVMsManager { @Inject private UserVmDao userVmDao; @Inject + private NetworkOfferingDao networkOfferingDao; + @Inject + EntityManager entityMgr; + @Inject + private NetworkOrchestrationService networkMgr; + @Inject + private PhysicalNetworkDao physicalNetworkDao; + @Inject + private IpAddressManager ipAddressManager; + @Inject + private StoragePoolHostDao storagePoolHostDao; + @Inject private HypervisorGuruManager hypervisorGuruManager; @Inject private VmwareDatacenterDao vmwareDatacenterDao; @Inject private ImageStoreDao imageStoreDao; @Inject - private StoragePoolHostDao storagePoolHostDao; - @Inject private DataStoreManager dataStoreManager; protected Gson gson; @@ -250,10 +282,11 @@ public UnmanagedVMsManagerImpl() { gson = GsonHelper.getGsonLogger(); } - private VMTemplateVO createDefaultDummyVmImportTemplate() { + private VMTemplateVO createDefaultDummyVmImportTemplate(boolean isKVM) { + String templateName = (isKVM) ? KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME : VM_IMPORT_DEFAULT_TEMPLATE_NAME; VMTemplateVO template = null; try { - template = VMTemplateVO.createSystemIso(templateDao.getNextInSequence(Long.class, "id"), VM_IMPORT_DEFAULT_TEMPLATE_NAME, VM_IMPORT_DEFAULT_TEMPLATE_NAME, true, + template = VMTemplateVO.createSystemIso(templateDao.getNextInSequence(Long.class, "id"), templateName, templateName, true, "", true, 64, Account.ACCOUNT_ID_SYSTEM, "", "VM Import Default Template", false, 1); template.setState(VirtualMachineTemplate.State.Inactive); @@ -262,13 +295,75 @@ private VMTemplateVO createDefaultDummyVmImportTemplate() { return null; } templateDao.remove(template.getId()); - template = templateDao.findByName(VM_IMPORT_DEFAULT_TEMPLATE_NAME); + template = templateDao.findByName(templateName); } catch (Exception e) { LOGGER.error("Unable to create default dummy template for VM import", e); } return template; } + private UnmanagedInstanceResponse createUnmanagedInstanceResponse(UnmanagedInstanceTO instance, Cluster cluster, Host host) { + UnmanagedInstanceResponse response = new UnmanagedInstanceResponse(); + response.setName(instance.getName()); + + if (cluster != null) { + response.setClusterId(cluster.getUuid()); + } + if (host != null) { + response.setHostId(host.getUuid()); + response.setHostName(host.getName()); + } + response.setPowerState(instance.getPowerState().toString()); + response.setCpuCores(instance.getCpuCores()); + response.setCpuSpeed(instance.getCpuSpeed()); + response.setCpuCoresPerSocket(instance.getCpuCoresPerSocket()); + response.setMemory(instance.getMemory()); + response.setOperatingSystemId(instance.getOperatingSystemId()); + response.setOperatingSystem(instance.getOperatingSystem()); + response.setObjectName("unmanagedinstance"); + + if (instance.getDisks() != null) { + for (UnmanagedInstanceTO.Disk disk : instance.getDisks()) { + UnmanagedInstanceDiskResponse diskResponse = new UnmanagedInstanceDiskResponse(); + diskResponse.setDiskId(disk.getDiskId()); + if (StringUtils.isNotEmpty(disk.getLabel())) { + diskResponse.setLabel(disk.getLabel()); + } + diskResponse.setCapacity(disk.getCapacity()); + diskResponse.setController(disk.getController()); + diskResponse.setControllerUnit(disk.getControllerUnit()); + diskResponse.setPosition(disk.getPosition()); + diskResponse.setImagePath(disk.getImagePath()); + diskResponse.setDatastoreName(disk.getDatastoreName()); + diskResponse.setDatastoreHost(disk.getDatastoreHost()); + diskResponse.setDatastorePath(disk.getDatastorePath()); + diskResponse.setDatastoreType(disk.getDatastoreType()); + response.addDisk(diskResponse); + } + } + + if (instance.getNics() != null) { + for (UnmanagedInstanceTO.Nic nic : instance.getNics()) { + NicResponse nicResponse = new NicResponse(); + nicResponse.setId(nic.getNicId()); + nicResponse.setNetworkName(nic.getNetwork()); + nicResponse.setMacAddress(nic.getMacAddress()); + if (StringUtils.isNotEmpty(nic.getAdapterType())) { + nicResponse.setAdapterType(nic.getAdapterType()); + } + if (!CollectionUtils.isEmpty(nic.getIpAddress())) { + nicResponse.setIpAddresses(nic.getIpAddress()); + } + nicResponse.setVlanId(nic.getVlan()); + nicResponse.setIsolatedPvlanId(nic.getPvlan()); + nicResponse.setIsolatedPvlanType(nic.getPvlanType()); + response.addNic(nicResponse); + } + } + + return response; + } + private List getAdditionalNameFilters(Cluster cluster) { List additionalNameFilter = new ArrayList<>(); if (cluster == null) { @@ -358,7 +453,7 @@ private boolean storagePoolSupportsDiskOffering(StoragePool pool, DiskOffering d return volumeApiService.doesTargetStorageSupportDiskOffering(pool, diskOffering.getTags()); } - private ServiceOfferingVO getUnmanagedInstanceServiceOffering(final UnmanagedInstanceTO instance, ServiceOfferingVO serviceOffering, final Account owner, final DataCenter zone, final Map details) + private ServiceOfferingVO getUnmanagedInstanceServiceOffering(final UnmanagedInstanceTO instance, ServiceOfferingVO serviceOffering, final Account owner, final DataCenter zone, final Map details, Hypervisor.HypervisorType hypervisorType) throws ServerApiException, PermissionDeniedException, ResourceAllocationException { if (instance == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, "Cannot find VM to import."); @@ -402,7 +497,7 @@ private ServiceOfferingVO getUnmanagedInstanceServiceOffering(final UnmanagedIns if (!memory.equals(serviceOffering.getRamSize()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering (%s) %dMB memory does not match VM memory %dMB and VM is not in powered off state (Power state: %s)", serviceOffering.getUuid(), serviceOffering.getRamSize(), memory, instance.getPowerState())); } - if (cpuSpeed != null && cpuSpeed > 0 && !cpuSpeed.equals(serviceOffering.getSpeed()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) { + if (hypervisorType == Hypervisor.HypervisorType.VMware && cpuSpeed != null && cpuSpeed > 0 && !cpuSpeed.equals(serviceOffering.getSpeed()) && !instance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOff)) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Service offering (%s) %dMHz CPU speed does not match VM CPU speed %dMHz and VM is not in powered off state (Power state: %s)", serviceOffering.getUuid(), serviceOffering.getSpeed(), cpuSpeed, instance.getPowerState())); } } @@ -468,16 +563,15 @@ private StoragePool getStoragePool(final UnmanagedInstanceTO.Disk disk, final Da return storagePool; } - private Pair> getRootAndDataDisks(List disks, final Map dataDiskOfferingMap) { + private Pair> getRootAndDataDisks( + List disks, + final Map dataDiskOfferingMap) { UnmanagedInstanceTO.Disk rootDisk = null; List dataDisks = new ArrayList<>(); - if (disks.size() == 1) { - rootDisk = disks.get(0); - return new Pair<>(rootDisk, dataDisks); - } + Set callerDiskIds = dataDiskOfferingMap.keySet(); if (callerDiskIds.size() != disks.size() - 1) { - String msg = String.format("VM has total %d disks for which %d disk offering mappings provided. %d disks need a disk offering for import", disks.size(), callerDiskIds.size(), disks.size()-1); + String msg = String.format("VM has total %d disks for which %d disk offering mappings provided. %d disks need a disk offering for import", disks.size(), callerDiskIds.size(), disks.size() - 1); LOGGER.error(String.format("%s. %s parameter can be used to provide disk offerings for the disks", msg, ApiConstants.DATADISK_OFFERING_LIST)); throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, msg); } @@ -489,11 +583,16 @@ private Pair> getRootAn rootDisk = disk; } else { dataDisks.add(disk); + DiskOffering diskOffering = diskOfferingDao.findById(dataDiskOfferingMap.getOrDefault(disk.getDiskId(), null)); + if ((disk.getCapacity() == null || disk.getCapacity() <= 0) && diskOffering != null) { + disk.setCapacity(diskOffering.getDiskSize()); + } } } - if (diskIdsWithoutOffering.size() > 1) { - throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM has total %d disks, disk offering mapping not provided for %d disks. Disk IDs that may need a disk offering - %s", disks.size(), diskIdsWithoutOffering.size()-1, String.join(", ", diskIdsWithoutOffering))); + if (diskIdsWithoutOffering.size() > 1 || rootDisk == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM has total %d disks, disk offering mapping not provided for %d disks. Disk IDs that may need a disk offering - %s", disks.size(), diskIdsWithoutOffering.size() - 1, String.join(", ", diskIdsWithoutOffering))); } + return new Pair<>(rootDisk, dataDisks); } @@ -551,7 +650,10 @@ private void checkUnmanagedNicAndNetworkForImport(String instanceName, Unmanaged if (!autoAssign && network.getGuestType().equals(Network.GuestType.Isolated)) { return; } + checksOnlyNeededForVmware(nic, network, hypervisorType); + } + private void checksOnlyNeededForVmware(UnmanagedInstanceTO.Nic nic, Network network, final Hypervisor.HypervisorType hypervisorType) { if (hypervisorType == Hypervisor.HypervisorType.VMware) { String networkBroadcastUri = network.getBroadcastUri() == null ? null : network.getBroadcastUri().toString(); if (nic.getVlan() != null && nic.getVlan() != 0 && nic.getPvlan() == null && @@ -600,7 +702,9 @@ private void checkUnmanagedNicIpAndNetworkForImport(String instanceName, Unmanag } } - private Map getUnmanagedNicNetworkMap(String instanceName, List nics, final Map callerNicNetworkMap, final Map callerNicIpAddressMap, final DataCenter zone, final String hostName, final Account owner, Hypervisor.HypervisorType hypervisorType) throws ServerApiException { + private Map getUnmanagedNicNetworkMap(String instanceName, List nics, final Map callerNicNetworkMap, + final Map callerNicIpAddressMap, final DataCenter zone, final String hostName, + final Account owner, Hypervisor.HypervisorType hypervisorType) throws ServerApiException { Map nicNetworkMap = new HashMap<>(); String nicAdapter = null; for (int i = 0; i < nics.size(); i++) { @@ -656,6 +760,81 @@ private Map getUnmanagedNicNetworkMap(String instanceName, List importExternalDisk(UnmanagedInstanceTO.Disk disk, VirtualMachine vm, DeployDestination dest, DiskOffering diskOffering, + Volume.Type type, VirtualMachineTemplate template,Long deviceId, String remoteUrl, String username, String password, + String tmpPath, DiskProfile diskProfile) { + final String path = StringUtils.isEmpty(disk.getDatastorePath()) ? disk.getImagePath() : disk.getDatastorePath(); + String chainInfo = disk.getChainInfo(); + if (StringUtils.isEmpty(chainInfo)) { + VirtualMachineDiskInfo diskInfo = new VirtualMachineDiskInfo(); + diskInfo.setDiskDeviceBusName(String.format("%s%d:%d", disk.getController(), disk.getControllerUnit(), disk.getPosition())); + diskInfo.setDiskChain(new String[]{disk.getImagePath()}); + chainInfo = gson.toJson(diskInfo); + } + Map storage = dest.getStorageForDisks(); + Volume volume = volumeDao.findById(diskProfile.getVolumeId()); + StoragePool storagePool = storage.get(volume); + CopyRemoteVolumeCommand copyRemoteVolumeCommand = new CopyRemoteVolumeCommand(); + copyRemoteVolumeCommand.setRemoteIp(remoteUrl); + copyRemoteVolumeCommand.setUsername(username); + copyRemoteVolumeCommand.setPassword(password); + copyRemoteVolumeCommand.setSrcFile(path); + StorageFilerTO storageTO = new StorageFilerTO(storagePool); + copyRemoteVolumeCommand.setStorageFilerTO(storageTO); + if(tmpPath == null || tmpPath.length() < 1) { + tmpPath = "/tmp/"; + } else { + // Add / if path doesn't end with / + if(tmpPath.charAt(tmpPath.length() - 1) != '/') { + tmpPath += "/"; + } + } + copyRemoteVolumeCommand.setTempPath(tmpPath); + Answer answer = agentManager.easySend(dest.getHost().getId(), copyRemoteVolumeCommand); + if (!(answer instanceof CopyRemoteVolumeAnswer)) { + throw new CloudRuntimeException("Error while copying volume"); + } + CopyRemoteVolumeAnswer copyRemoteVolumeAnswer = (CopyRemoteVolumeAnswer) answer; + if(!copyRemoteVolumeAnswer.getResult()) { + throw new CloudRuntimeException("Error while copying volume"); + } + diskProfile.setSize(copyRemoteVolumeAnswer.getSize()); + DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId, + storagePool.getId(), copyRemoteVolumeAnswer.getFilename(), chainInfo, diskProfile); + + return new Pair<>(profile, storagePool); + } + + private Pair importKVMLocalDisk(VirtualMachine vm, DiskOffering diskOffering, + Volume.Type type, VirtualMachineTemplate template, + Long deviceId, Long hostId, String diskPath, DiskProfile diskProfile) { + List storagePools = primaryDataStoreDao.findLocalStoragePoolsByHostAndTags(hostId, null); + + if(storagePools.size() < 1) { + throw new CloudRuntimeException("Local Storage not found for host"); + } + + StoragePool storagePool = storagePools.get(0); + + DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId, + storagePool.getId(), diskPath, null, diskProfile); + + return new Pair<>(profile, storagePool); + } + + + private Pair importKVMSharedDisk(VirtualMachine vm, DiskOffering diskOffering, + Volume.Type type, VirtualMachineTemplate template, + Long deviceId, Long poolId, String diskPath, DiskProfile diskProfile) { + StoragePool storagePool = primaryDataStoreDao.findById(poolId); + + DiskProfile profile = volumeManager.updateImportedVolume(type, diskOffering, vm, template, deviceId, + poolId, diskPath, null, diskProfile); + + return new Pair<>(profile, storagePool); + } + + private Pair importDisk(UnmanagedInstanceTO.Disk disk, VirtualMachine vm, Cluster cluster, DiskOffering diskOffering, Volume.Type type, String name, Long diskSize, Long minIops, Long maxIops, VirtualMachineTemplate template, Account owner, Long deviceId) { @@ -779,7 +958,7 @@ private UserVm migrateImportedVM(HostVO sourceHost, VirtualMachineTemplate templ for (StoragePool pool : storagePools) { if (diskProfileStoragePool.second().getId() != pool.getId() && storagePoolSupportsDiskOffering(pool, dOffering) - ) { + ) { storagePool = pool; break; } @@ -791,7 +970,7 @@ private UserVm migrateImportedVM(HostVO sourceHost, VirtualMachineTemplate templ for (StoragePool pool : storagePools) { if (diskProfileStoragePool.second().getId() != pool.getId() && storagePoolSupportsDiskOffering(pool, dOffering) - ) { + ) { storagePool = pool; break; } @@ -888,10 +1067,9 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI LOGGER.debug(LogUtils.logGsonWithoutException("Trying to import VM [%s] with name [%s], in zone [%s], cluster [%s], and host [%s], using template [%s], service offering [%s], disks map [%s], NICs map [%s] and details [%s].", unmanagedInstance, instanceName, zone, cluster, host, template, serviceOffering, dataDiskOfferingMap, nicNetworkMap, details)); UserVm userVm = null; - ServiceOfferingVO validatedServiceOffering = null; try { - validatedServiceOffering = getUnmanagedInstanceServiceOffering(unmanagedInstance, serviceOffering, owner, zone, details); + validatedServiceOffering = getUnmanagedInstanceServiceOffering(unmanagedInstance, serviceOffering, owner, zone, details, cluster.getHypervisorType()); } catch (Exception e) { String errorMsg = String.format("Failed to import Unmanaged VM [%s] because the service offering [%s] is not compatible due to [%s].", unmanagedInstance.getName(), serviceOffering.getUuid(), StringUtils.defaultIfEmpty(e.getMessage(), "")); LOGGER.error(errorMsg, e); @@ -899,7 +1077,7 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI } String internalCSName = unmanagedInstance.getInternalCSName(); - if(StringUtils.isEmpty(internalCSName)){ + if (StringUtils.isEmpty(internalCSName)) { internalCSName = instanceName; } Map allDetails = new HashMap<>(details); @@ -925,7 +1103,16 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI if (rootDisk == null || StringUtils.isEmpty(rootDisk.getController())) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed. Unable to retrieve root disk details for VM: %s ", instanceName)); } + if (cluster.getHypervisorType() == Hypervisor.HypervisorType.KVM) { + Long rootDiskOfferingId = validatedServiceOffering.getDiskOfferingId(); + DiskOffering rootDiskOffering = diskOfferingDao.findById(rootDiskOfferingId); + if ((rootDisk.getCapacity() == null || rootDisk.getCapacity() <= 0) && rootDiskOffering != null) { + rootDisk.setCapacity(rootDiskOffering.getDiskSize()); + } + } allDetails.put(VmDetailConstants.ROOT_DISK_CONTROLLER, rootDisk.getController()); + allDetails.put(VmDetailConstants.ROOT_DISK_SIZE, String.valueOf(rootDisk.getCapacity() / Resource.ResourceType.bytesToGiB)); + try { checkUnmanagedDiskAndOfferingForImport(unmanagedInstance.getName(), rootDisk, null, validatedServiceOffering, owner, zone, cluster, migrateAllowed); if (CollectionUtils.isNotEmpty(dataDisks)) { // Data disk(s) present @@ -943,20 +1130,27 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI if (!CollectionUtils.isEmpty(unmanagedInstance.getNics())) { allDetails.put(VmDetailConstants.NIC_ADAPTER, unmanagedInstance.getNics().get(0).getAdapterType()); } + + if (StringUtils.isNotEmpty(unmanagedInstance.getVncPassword())) { + allDetails.put(VmDetailConstants.KVM_VNC_PASSWORD, unmanagedInstance.getVncPassword()); + } + VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff; if (unmanagedInstance.getPowerState().equals(UnmanagedInstanceTO.PowerState.PowerOn)) { powerState = VirtualMachine.PowerState.PowerOn; } + try { userVm = userVmManager.importVM(zone, host, template, internalCSName, displayName, owner, null, caller, true, null, owner.getAccountId(), userId, validatedServiceOffering, null, hostName, - cluster.getHypervisorType(), allDetails, powerState); + cluster.getHypervisorType(), allDetails, powerState, null); } catch (InsufficientCapacityException ice) { String errorMsg = String.format("Failed to import VM [%s] due to [%s].", instanceName, ice.getMessage()); LOGGER.error(errorMsg, ice); throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, errorMsg); } + if (userVm == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); } @@ -998,7 +1192,7 @@ private UserVm importVirtualMachineInternal(final UnmanagedInstanceTO unmanagedI for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) { Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId())); Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId()); - importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex==0, forced); + importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex == 0, forced); nicIndex++; } } catch (Exception e) { @@ -1023,7 +1217,7 @@ private HashMap getUnmanagedInstancesForHost(HostVO command.setInstanceName(instanceName); command.setManagedInstancesNames(managedVms); Answer answer = agentManager.easySend(host.getId(), command); - if (!(answer instanceof GetUnmanagedInstancesAnswer)) { + if (!(answer instanceof GetUnmanagedInstancesAnswer) || !answer.getResult()) { return unmanagedInstances; } GetUnmanagedInstancesAnswer unmanagedInstancesAnswer = (GetUnmanagedInstancesAnswer) answer; @@ -1043,6 +1237,7 @@ protected Cluster basicAccessChecks(Long clusterId) { if (cluster == null) { throw new InvalidParameterValueException(String.format("Cluster with ID [%d] cannot be found.", clusterId)); } + if (!importUnmanagedInstancesSupportedHypervisors.contains(cluster.getHypervisorType())) { throw new InvalidParameterValueException(String.format("VM import is currently not supported for hypervisor [%s].", cluster.getHypervisorType().toString())); } @@ -1235,7 +1430,7 @@ protected VMTemplateVO getTemplateForImportInstance(Long templateId, Hypervisor. if (templateId == null) { template = templateDao.findByName(VM_IMPORT_DEFAULT_TEMPLATE_NAME); if (template == null) { - template = createDefaultDummyVmImportTemplate(); + template = createDefaultDummyVmImportTemplate(false); if (template == null) { throw new InvalidParameterValueException(String.format("Default VM import template with unique name: %s for hypervisor: %s cannot be created. Please use templateid parameter for import", VM_IMPORT_DEFAULT_TEMPLATE_NAME, hypervisorType.toString())); } @@ -1252,7 +1447,13 @@ protected VMTemplateVO getTemplateForImportInstance(Long templateId, Hypervisor. @Override @ActionEvent(eventType = EventTypes.EVENT_VM_IMPORT, eventDescription = "importing VM", async = true) public UserVmResponse importVm(ImportVmCmd cmd) { - return baseImportInstance(cmd); + String source = cmd.getImportSource().toUpperCase(); + ImportSource importSource = Enum.valueOf(ImportSource.class, source); + if (ImportSource.VMWARE == importSource || ImportSource.UNMANAGED == importSource) { + return baseImportInstance(cmd); + } else { + return importKvmInstance(cmd); + } } private UserVm importUnmanagedInstanceFromVmwareToVmware(DataCenter zone, Cluster cluster, @@ -1277,12 +1478,18 @@ private UserVm importUnmanagedInstanceFromVmwareToVmware(DataCenter zone, Cluste if (unmanagedInstance == null) { throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve details for unmanaged VM: %s", name)); } + + if (template.getName().equals(VM_IMPORT_DEFAULT_TEMPLATE_NAME) && cluster.getHypervisorType().equals(Hypervisor.HypervisorType.KVM)) { + throw new InvalidParameterValueException("Template is needed and unable to use default template for hypervisor " + host.getHypervisorType().toString()); + } + if (template.getName().equals(VM_IMPORT_DEFAULT_TEMPLATE_NAME)) { String osName = unmanagedInstance.getOperatingSystem(); GuestOS guestOS = null; if (StringUtils.isNotEmpty(osName)) { guestOS = guestOSDao.findOneByDisplayName(osName); } + GuestOSHypervisor guestOSHypervisor = null; if (guestOS != null) { guestOSHypervisor = guestOSHypervisorDao.findByOsIdAndHypervisor(guestOS.getId(), host.getHypervisorType().toString(), host.getHypervisorVersion()); @@ -1296,6 +1503,7 @@ private UserVm importUnmanagedInstanceFromVmwareToVmware(DataCenter zone, Cluste } throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Unable to retrieve guest OS details for unmanaged VM: %s with OS name: %s, OS ID: %s for hypervisor: %s version: %s. templateid parameter can be used to assign template for VM", name, osName, unmanagedInstance.getOperatingSystemId(), host.getHypervisorType().toString(), host.getHypervisorVersion())); } + template.setGuestOSId(guestOSHypervisor.getGuestOsId()); } userVm = importVirtualMachineInternal(unmanagedInstance, instanceName, zone, cluster, host, @@ -1333,7 +1541,7 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster String username = cmd.getUsername(); String password = cmd.getPassword(); String clusterName = cmd.getClusterName(); - String sourceHostName = cmd.getHost(); + String sourceHostName = cmd.getHostIp(); Long convertInstanceHostId = cmd.getConvertInstanceHostId(); Long convertStoragePoolId = cmd.getConvertStoragePoolId(); @@ -1633,6 +1841,7 @@ public List> getCommands() { cmdList.add(ListUnmanagedInstancesCmd.class); cmdList.add(ImportUnmanagedInstanceCmd.class); cmdList.add(UnmanageVMInstanceCmd.class); + cmdList.add(ListVmsForImportCmd.class); cmdList.add(ImportVmCmd.class); return cmdList; } @@ -1708,8 +1917,9 @@ public boolean unmanageVMInstance(long vmId) { throw new InvalidParameterValueException("Could not find VM to unmanage, it is either removed or not existing VM"); } else if (vmVO.getState() != VirtualMachine.State.Running && vmVO.getState() != VirtualMachine.State.Stopped) { throw new InvalidParameterValueException("VM with id = " + vmVO.getUuid() + " must be running or stopped to be unmanaged"); - } else if (vmVO.getHypervisorType() != Hypervisor.HypervisorType.VMware) { - throw new UnsupportedServiceException("Unmanage VM is currently allowed for VMware VMs only"); + } else if (!UnmanagedVMsManager.isSupported(vmVO.getHypervisorType())) { + throw new UnsupportedServiceException("Unmanage VM is currently not allowed for hypervisor " + + vmVO.getHypervisorType().toString()); } else if (vmVO.getType() != VirtualMachine.Type.User) { throw new UnsupportedServiceException("Unmanage VM is currently allowed for guest VMs only"); } @@ -1743,6 +1953,552 @@ private boolean existsVMToUnmanage(String instanceName, Long hostId) { return answer.getResult(); } + private UserVmResponse importKvmInstance(ImportVmCmd cmd) { + final Account caller = CallContext.current().getCallingAccount(); + if (caller.getType() != Account.Type.ADMIN) { + throw new PermissionDeniedException(String.format("Cannot perform this operation, Calling account is not root admin: %s", caller.getUuid())); + } + final Long zoneId = cmd.getZoneId(); + final DataCenterVO zone = dataCenterDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Please specify a valid zone."); + } + final String hypervisorType = cmd.getHypervisor(); + if (!Hypervisor.HypervisorType.KVM.toString().equalsIgnoreCase(hypervisorType)) { + throw new InvalidParameterValueException(String.format("VM import is currently not supported for hypervisor: %s", hypervisorType)); + } + + final String instanceName = cmd.getName(); + if (StringUtils.isEmpty(instanceName)) { + throw new InvalidParameterValueException(String.format("Instance name cannot be empty")); + } + if (cmd.getDomainId() != null && StringUtils.isEmpty(cmd.getAccountName())) { + throw new InvalidParameterValueException("domainid parameter must be specified with account parameter"); + } + final Account owner = accountService.getActiveAccountById(cmd.getEntityOwnerId()); + long userId = CallContext.current().getCallingUserId(); + List userVOs = userDao.listByAccount(owner.getAccountId()); + if (CollectionUtils.isNotEmpty(userVOs)) { + userId = userVOs.get(0).getId(); + } + VMTemplateVO template = templateDao.findByName(KVM_VM_IMPORT_DEFAULT_TEMPLATE_NAME); + if (template == null) { + template = createDefaultDummyVmImportTemplate(true); + if (template == null) { + throw new InvalidParameterValueException("Error while creating default Import Vm Template"); + } + } + + final Long serviceOfferingId = cmd.getServiceOfferingId(); + if (serviceOfferingId == null) { + throw new InvalidParameterValueException(String.format("Service offering ID cannot be null")); + } + final ServiceOfferingVO serviceOffering = serviceOfferingDao.findById(serviceOfferingId); + if (serviceOffering == null) { + throw new InvalidParameterValueException(String.format("Service offering ID: %d cannot be found", serviceOfferingId)); + } + accountService.checkAccess(owner, serviceOffering, zone); + try { + resourceLimitService.checkResourceLimit(owner, Resource.ResourceType.user_vm, 1); + } catch (ResourceAllocationException e) { + LOGGER.error(String.format("VM resource allocation error for account: %s", owner.getUuid()), e); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM resource allocation error for account: %s. %s", owner.getUuid(), StringUtils.defaultString(e.getMessage()))); + } + String displayName = cmd.getDisplayName(); + if (StringUtils.isEmpty(displayName)) { + displayName = instanceName; + } + String hostName = cmd.getHostName(); + if (StringUtils.isEmpty(hostName)) { + if (!NetUtils.verifyDomainNameLabel(instanceName, true)) { + throw new InvalidParameterValueException(String.format("Please provide hostname for the VM. VM name contains unsupported characters for it to be used as hostname")); + } + hostName = instanceName; + } + if (!NetUtils.verifyDomainNameLabel(hostName, true)) { + throw new InvalidParameterValueException("Invalid VM hostname. VM hostname can contain ASCII letters 'a' through 'z', the digits '0' through '9', " + + "and the hyphen ('-'), must be between 1 and 63 characters long, and can't start or end with \"-\" and can't start with digit"); + } + + final Map nicNetworkMap = cmd.getNicNetworkList(); + final Map nicIpAddressMap = cmd.getNicIpAddressList(); + final Map dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList(); + final Map details = cmd.getDetails(); + + String remoteUrl = cmd.getHost(); + String source = cmd.getImportSource().toUpperCase(); + String diskPath = cmd.getDiskPath(); + ImportSource importSource = Enum.valueOf(ImportSource.class, source); + Long hostId = cmd.getHostId(); + Long poolId = cmd.getStoragePoolId(); + Long networkId = cmd.getNetworkId(); + + UnmanagedInstanceTO unmanagedInstanceTO = null; + if (ImportSource.EXTERNAL == importSource) { + if (StringUtils.isBlank(cmd.getUsername())) { + throw new InvalidParameterValueException("Username need to be provided."); + } + + HashMap instancesMap = getRemoteVms(zoneId, remoteUrl, cmd.getUsername(), cmd.getPassword()); + unmanagedInstanceTO = instancesMap.get(cmd.getName()); + if (unmanagedInstanceTO == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Vm with name: %s not found on remote host", instanceName)); + } + } + + if (ImportSource.SHARED == importSource || ImportSource.LOCAL == importSource) { + if (diskPath == null) { + throw new InvalidParameterValueException("Disk Path is required for Import from shared/local storage"); + } + + if (networkId == null) { + throw new InvalidParameterValueException("Network is required for Import from shared/local storage"); + } + + if (poolId == null) { + throw new InvalidParameterValueException("Storage Pool is required for Import from shared/local storage"); + } + + StoragePool storagePool = primaryDataStoreDao.findById(poolId); + if (storagePool == null) { + throw new InvalidParameterValueException("Storage Pool not found"); + } + + if (volumeDao.findByPoolIdAndPath(poolId, diskPath) != null) { + throw new InvalidParameterValueException("Disk image is already in use"); + } + + DiskOffering diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + + if (diskOffering != null && !storagePoolSupportsDiskOffering(storagePool, diskOffering)) { + throw new InvalidParameterValueException(String.format("Service offering: %s storage tags are not compatible with selected storage pool: %s", serviceOffering.getUuid(), storagePool.getUuid())); + } + } + + if (ImportSource.LOCAL == importSource) { + if (hostId == null) { + throw new InvalidParameterValueException("Host is required for Import from local storage"); + } + + if (hostDao.findById(hostId) == null) { + throw new InvalidParameterValueException("Host not found"); + } + + if(storagePoolHostDao.findByPoolHost(poolId, hostId) == null) { + throw new InvalidParameterValueException("Specified Local Storage Pool not found on Host"); + } + } + + UserVm userVm = null; + + if (ImportSource.EXTERNAL == importSource) { + String username = cmd.getUsername(); + String password = cmd.getPassword(); + String tmpPath = cmd.getTmpPath(); + userVm = importExternalKvmVirtualMachine(unmanagedInstanceTO, instanceName, zone, + template, displayName, hostName, caller, owner, userId, + serviceOffering, dataDiskOfferingMap, + nicNetworkMap, nicIpAddressMap, remoteUrl, username, password, tmpPath, details); + } else if (ImportSource.SHARED == importSource || ImportSource.LOCAL == importSource) { + try { + userVm = importKvmVirtualMachineFromDisk(importSource, instanceName, zone, + template, displayName, hostName, caller, owner, userId, + serviceOffering, dataDiskOfferingMap, networkId, hostId, poolId, diskPath, + details); + } catch (InsufficientCapacityException e) { + throw new RuntimeException(e); + } catch (ResourceAllocationException e) { + throw new RuntimeException(e); + } + } + if (userVm == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import Vm with name: %s ", instanceName)); + } + + CallContext.current().setEventResourceId(userVm.getId()); + CallContext.current().setEventResourceType(ApiCommandResourceType.VirtualMachine); + return responseGenerator.createUserVmResponse(ResponseObject.ResponseView.Full, "virtualmachine", userVm).get(0); + } + + private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanagedInstance, final String instanceName, final DataCenter zone, + final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId, + final ServiceOfferingVO serviceOffering, final Map dataDiskOfferingMap, + final Map nicNetworkMap, final Map callerNicIpAddressMap, + final String remoteUrl, String username, String password, String tmpPath, final Map details) { + UserVm userVm = null; + + Map allDetails = new HashMap<>(details); + if (serviceOffering.isDynamic()) { + allDetails.put(VmDetailConstants.CPU_NUMBER, String.valueOf(serviceOffering.getCpu())); + allDetails.put(VmDetailConstants.MEMORY, String.valueOf(serviceOffering.getRamSize())); + allDetails.put(VmDetailConstants.CPU_SPEED, String.valueOf(serviceOffering.getSpeed())); + } + // Check disks and supplied disk offerings + List unmanagedInstanceDisks = unmanagedInstance.getDisks(); + + if (CollectionUtils.isEmpty(unmanagedInstanceDisks)) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("No attached disks found for the unmanaged VM: %s", instanceName)); + } + + Pair> rootAndDataDisksPair = getRootAndDataDisks(unmanagedInstanceDisks, dataDiskOfferingMap); + final UnmanagedInstanceTO.Disk rootDisk = rootAndDataDisksPair.first(); + final List dataDisks = rootAndDataDisksPair.second(); + if (rootDisk == null || StringUtils.isEmpty(rootDisk.getController())) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("VM import failed. Unable to retrieve root disk details for VM: %s ", instanceName)); + } + allDetails.put(VmDetailConstants.ROOT_DISK_CONTROLLER, rootDisk.getController()); + + // Check NICs and supplied networks + Map nicIpAddressMap = getNicIpAddresses(unmanagedInstance.getNics(), callerNicIpAddressMap); + Map allNicNetworkMap = getUnmanagedNicNetworkMap(unmanagedInstance.getName(), unmanagedInstance.getNics(), nicNetworkMap, nicIpAddressMap, zone, hostName, owner, Hypervisor.HypervisorType.KVM); + if (!CollectionUtils.isEmpty(unmanagedInstance.getNics())) { + allDetails.put(VmDetailConstants.NIC_ADAPTER, unmanagedInstance.getNics().get(0).getAdapterType()); + } + VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff; + + String internalName = getInternalName(owner.getAccountId()); + + try { + userVm = userVmManager.importVM(zone, null, template, internalName, displayName, owner, + null, caller, true, null, owner.getAccountId(), userId, + serviceOffering, null, hostName, + Hypervisor.HypervisorType.KVM, allDetails, powerState, null); + } catch (InsufficientCapacityException ice) { + LOGGER.error(String.format("Failed to import vm name: %s", instanceName), ice); + throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); + } + if (userVm == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); + } + DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); + + DiskProfile[] dataDiskProfiles = new DiskProfile[dataDisks.size()]; + int diskSeq = 0; + for (UnmanagedInstanceTO.Disk disk : dataDisks) { + if (disk.getCapacity() == null || disk.getCapacity() == 0) { + throw new InvalidParameterValueException(String.format("Disk ID: %s size is invalid", disk.getDiskId())); + } + DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); + DiskProfile dataDiskProfile = volumeManager.allocateRawVolume(Volume.Type.DATADISK, String.format("DATA-%d-%s", userVm.getId(), disk.getDiskId()), offering, null, null, null, userVm, template, owner, null); + dataDiskProfiles[diskSeq++] = dataDiskProfile; + } + + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); + DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); + final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, null, null, null); + DeployDestination dest = null; + try { + dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); + } catch (Exception e) { + LOGGER.warn(String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName()), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); + } + if(dest == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); + } + + List> diskProfileStoragePoolList = new ArrayList<>(); + try { + if (rootDisk.getCapacity() == null || rootDisk.getCapacity() == 0) { + throw new InvalidParameterValueException(String.format("Root disk ID: %s size is invalid", rootDisk.getDiskId())); + } + + diskProfileStoragePoolList.add(importExternalDisk(rootDisk, userVm, dest, diskOffering, Volume.Type.ROOT, + template, null, remoteUrl, username, password, tmpPath, diskProfile)); + + long deviceId = 1L; + diskSeq = 0; + for (UnmanagedInstanceTO.Disk disk : dataDisks) { + DiskProfile dataDiskProfile = dataDiskProfiles[diskSeq++]; + DiskOffering offering = diskOfferingDao.findById(dataDiskOfferingMap.get(disk.getDiskId())); + + diskProfileStoragePoolList.add(importExternalDisk(disk, userVm, dest, offering, Volume.Type.DATADISK, + template, deviceId, remoteUrl, username, password, tmpPath, dataDiskProfile)); + deviceId++; + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + try { + int nicIndex = 0; + for (UnmanagedInstanceTO.Nic nic : unmanagedInstance.getNics()) { + Network network = networkDao.findById(allNicNetworkMap.get(nic.getNicId())); + Network.IpAddresses ipAddresses = nicIpAddressMap.get(nic.getNicId()); + importNic(nic, userVm, network, ipAddresses, nicIndex, nicIndex==0, true); + nicIndex++; + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to import NICs while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import NICs while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + publishVMUsageUpdateResourceCount(userVm, serviceOffering); + return userVm; + } + + private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, final String instanceName, final DataCenter zone, + final VirtualMachineTemplate template, final String displayName, final String hostName, final Account caller, final Account owner, final Long userId, + final ServiceOfferingVO serviceOffering, final Map dataDiskOfferingMap, final Long networkId, + final Long hostId, final Long poolId, final String diskPath, final Map details) throws InsufficientCapacityException, ResourceAllocationException { + + UserVm userVm = null; + + Map allDetails = new HashMap<>(details); + if (serviceOffering.isDynamic()) { + allDetails.put(VmDetailConstants.CPU_NUMBER, String.valueOf(serviceOffering.getCpu())); + allDetails.put(VmDetailConstants.MEMORY, String.valueOf(serviceOffering.getRamSize())); + allDetails.put(VmDetailConstants.CPU_SPEED, String.valueOf(serviceOffering.getSpeed())); + } + + VirtualMachine.PowerState powerState = VirtualMachine.PowerState.PowerOff; + + NetworkVO network = networkDao.findById(networkId); + if (network == null) { + throw new InvalidParameterValueException("Unable to find network by id " + networkId); + } + + networkModel.checkNetworkPermissions(owner, network); + + // don't allow to use system networks + NetworkOffering networkOffering = entityMgr.findById(NetworkOffering.class, network.getNetworkOfferingId()); + if (networkOffering.isSystemOnly()) { + throw new InvalidParameterValueException("Network id=" + networkId + " is system only and can't be used for vm deployment"); + } + + LinkedHashMap> networkNicMap = new LinkedHashMap<>(); + + if ((network.getDataCenterId() != zone.getId())) { + if (!network.isStrechedL2Network()) { + throw new InvalidParameterValueException("Network id=" + network.getId() + + " doesn't belong to zone " + zone.getId()); + } + } + + String macAddress = networkModel.getNextAvailableMacAddressInNetwork(networkId); + String ipAddress = null; + if (network.getGuestType() != Network.GuestType.L2) { + ipAddress = ipAddressManager.acquireGuestIpAddress(network, null); + } + + Network.IpAddresses requestedIpPair = new Network.IpAddresses(ipAddress, null, macAddress); + + NicProfile nicProfile = new NicProfile(requestedIpPair.getIp4Address(), requestedIpPair.getIp6Address(), requestedIpPair.getMacAddress()); + nicProfile.setOrderIndex(0); + + boolean securityGroupEnabled = false; + if (networkModel.isSecurityGroupSupportedInNetwork(network)) { + securityGroupEnabled = true; + } + List profiles = networkNicMap.get(network.getUuid()); + if (CollectionUtils.isEmpty(profiles)) { + profiles = new ArrayList<>(); + } + profiles.add(nicProfile); + networkNicMap.put(network.getUuid(), profiles); + + String internalName = getInternalName(owner.getAccountId()); + + try { + userVm = userVmManager.importVM(zone, null, template, internalName, displayName, owner, + null, caller, true, null, owner.getAccountId(), userId, + serviceOffering, null, hostName, + Hypervisor.HypervisorType.KVM, allDetails, powerState, networkNicMap); + } catch (InsufficientCapacityException ice) { + LOGGER.error(String.format("Failed to import vm name: %s", instanceName), ice); + throw new ServerApiException(ApiErrorCode.INSUFFICIENT_CAPACITY_ERROR, ice.getMessage()); + } + if (userVm == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import vm name: %s", instanceName)); + } + DiskOfferingVO diskOffering = diskOfferingDao.findById(serviceOffering.getDiskOfferingId()); + String rootVolumeName = String.format("ROOT-%s", userVm.getId()); + DiskProfile diskProfile = volumeManager.allocateRawVolume(Volume.Type.ROOT, rootVolumeName, diskOffering, null, null, null, userVm, template, owner, null); + + final VirtualMachineProfile profile = new VirtualMachineProfileImpl(userVm, template, serviceOffering, owner, null); + DeploymentPlanner.ExcludeList excludeList = new DeploymentPlanner.ExcludeList(); + final DataCenterDeployment plan = new DataCenterDeployment(zone.getId(), null, null, hostId, poolId, null); + DeployDestination dest = null; + try { + dest = deploymentPlanningManager.planDeployment(profile, plan, excludeList, null); + } catch (Exception e) { + LOGGER.warn(String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName()), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s while finding deployment destination", userVm.getInstanceName())); + } + if(dest == null) { + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Import failed for Vm: %s. Suitable deployment destination not found", userVm.getInstanceName())); + } + + + Map storage = dest.getStorageForDisks(); + Volume volume = volumeDao.findById(diskProfile.getVolumeId()); + StoragePool storagePool = storage.get(volume); + CheckVolumeCommand checkVolumeCommand = new CheckVolumeCommand(); + checkVolumeCommand.setSrcFile(diskPath); + StorageFilerTO storageTO = new StorageFilerTO(storagePool); + checkVolumeCommand.setStorageFilerTO(storageTO); + Answer answer = agentManager.easySend(dest.getHost().getId(), checkVolumeCommand); + if (!(answer instanceof CheckVolumeAnswer)) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + CheckVolumeAnswer checkVolumeAnswer = (CheckVolumeAnswer) answer; + if(!checkVolumeAnswer.getResult()) { + cleanupFailedImportVM(userVm); + throw new CloudRuntimeException("Disk not found or is invalid"); + } + diskProfile.setSize(checkVolumeAnswer.getSize()); + + + List> diskProfileStoragePoolList = new ArrayList<>(); + try { + long deviceId = 1L; + if(ImportSource.SHARED == importSource) { + diskProfileStoragePoolList.add(importKVMSharedDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, poolId, diskPath, diskProfile)); + } else if(ImportSource.LOCAL == importSource) { + diskProfileStoragePoolList.add(importKVMLocalDisk(userVm, diskOffering, Volume.Type.ROOT, + template, deviceId, hostId, diskPath, diskProfile)); + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to import volumes while importing vm: %s", instanceName), e); + cleanupFailedImportVM(userVm); + throw new ServerApiException(ApiErrorCode.INTERNAL_ERROR, String.format("Failed to import volumes while importing vm: %s. %s", instanceName, StringUtils.defaultString(e.getMessage()))); + } + networkOrchestrationService.importNic(macAddress,0,network, true, userVm, requestedIpPair, true); + publishVMUsageUpdateResourceCount(userVm, serviceOffering); + return userVm; + } + + + private NetworkVO getDefaultNetwork(DataCenter zone, Account owner, boolean selectAny) throws InsufficientCapacityException, ResourceAllocationException { + NetworkVO defaultNetwork = null; + + // if no network is passed in + // Check if default virtual network offering has + // Availability=Required. If it's true, search for corresponding + // network + // * if network is found, use it. If more than 1 virtual network is + // found, throw an error + // * if network is not found, create a new one and use it + + List requiredOfferings = networkOfferingDao.listByAvailability(NetworkOffering.Availability.Required, false); + if (requiredOfferings.size() < 1) { + throw new InvalidParameterValueException("Unable to find network offering with availability=" + NetworkOffering.Availability.Required + + " to automatically create the network as a part of vm creation"); + } + + if (requiredOfferings.get(0).getState() == NetworkOffering.State.Enabled) { + // get Virtual networks + List virtualNetworks = networkModel.listNetworksForAccount(owner.getId(), zone.getId(), Network.GuestType.Isolated); + if (virtualNetworks == null) { + throw new InvalidParameterValueException("No (virtual) networks are found for account " + owner); + } + if (virtualNetworks.isEmpty()) { + defaultNetwork = createDefaultNetworkForAccount(zone, owner, requiredOfferings); + } else if (virtualNetworks.size() > 1 && !selectAny) { + throw new InvalidParameterValueException("More than 1 default Isolated networks are found for account " + owner + "; please specify networkIds"); + } else { + defaultNetwork = networkDao.findById(virtualNetworks.get(0).getId()); + } + } else { + throw new InvalidParameterValueException("Required network offering id=" + requiredOfferings.get(0).getId() + " is not in " + NetworkOffering.State.Enabled); + } + + return defaultNetwork; + } + + private NetworkVO createDefaultNetworkForAccount(DataCenter zone, Account owner, List requiredOfferings) + throws InsufficientCapacityException, ResourceAllocationException { + NetworkVO defaultNetwork = null; + long physicalNetworkId = networkModel.findPhysicalNetworkId(zone.getId(), requiredOfferings.get(0).getTags(), requiredOfferings.get(0).getTrafficType()); + // Validate physical network + PhysicalNetwork physicalNetwork = physicalNetworkDao.findById(physicalNetworkId); + if (physicalNetwork == null) { + throw new InvalidParameterValueException("Unable to find physical network with id: " + physicalNetworkId + " and tag: " + + requiredOfferings.get(0).getTags()); + } + LOGGER.debug("Creating network for account " + owner + " from the network offering id=" + requiredOfferings.get(0).getId() + " as a part of deployVM process"); + Network newNetwork = networkMgr.createGuestNetwork(requiredOfferings.get(0).getId(), owner.getAccountName() + "-network", owner.getAccountName() + "-network", + null, null, null, false, null, owner, null, physicalNetwork, zone.getId(), ControlledEntity.ACLType.Account, null, null, null, null, true, null, null, + null, null, null, null, null, null, null, null); + if (newNetwork != null) { + defaultNetwork = networkDao.findById(newNetwork.getId()); + } + return defaultNetwork; + } + + //generate unit test + public ListResponse listVmsForImport(ListVmsForImportCmd cmd) { + final Account caller = CallContext.current().getCallingAccount(); + if (caller.getType() != Account.Type.ADMIN) { + throw new PermissionDeniedException(String.format("Cannot perform this operation, Calling account is not root admin: %s", caller.getUuid())); + } + final Long zoneId = cmd.getZoneId(); + final DataCenterVO zone = dataCenterDao.findById(zoneId); + if (zone == null) { + throw new InvalidParameterValueException("Please specify a valid zone."); + } + final String hypervisorType = cmd.getHypervisor(); + if (Hypervisor.HypervisorType.KVM.toString().equalsIgnoreCase(hypervisorType)) { + if (StringUtils.isBlank(cmd.getUsername())) { + throw new InvalidParameterValueException("Username need to be provided."); + } + } else { + throw new InvalidParameterValueException(String.format("VM Import is currently not supported for hypervisor: %s", hypervisorType)); + } + + String keyword = cmd.getKeyword(); + if (StringUtils.isNotEmpty(keyword)) { + keyword = keyword.toLowerCase(); + } + + List responses = new ArrayList<>(); + HashMap vmMap = getRemoteVms(zoneId, cmd.getHost(), cmd.getUsername(), cmd.getPassword()); + for (String key : vmMap.keySet()) { + UnmanagedInstanceTO instance = vmMap.get(key); + if (StringUtils.isNotEmpty(keyword) && + !instance.getName().toLowerCase().contains(keyword)) { + continue; + } + responses.add(createUnmanagedInstanceResponse(instance, null, null)); + } + + ListResponse listResponses = new ListResponse<>(); + listResponses.setResponses(responses, responses.size()); + return listResponses; + } + + private HashMap getRemoteVms(long zoneId, String remoteUrl, String username, String password) { + //ToDo: add option to list one Vm by name + List hosts = resourceManager.listAllUpAndEnabledHostsInOneZoneByHypervisor(Hypervisor.HypervisorType.KVM, zoneId); + if(hosts.size() < 1) { + throw new CloudRuntimeException("No hosts available for Vm Import"); + } + HostVO host = hosts.get(0); + GetRemoteVmsCommand getRemoteVmsCommand = new GetRemoteVmsCommand(remoteUrl, username, password); + Answer answer = agentManager.easySend(host.getId(), getRemoteVmsCommand); + if (!(answer instanceof GetRemoteVmsAnswer)) { + throw new CloudRuntimeException("Error while listing remote Vms"); + } + GetRemoteVmsAnswer getRemoteVmsAnswer = (GetRemoteVmsAnswer) answer; + return getRemoteVmsAnswer.getUnmanagedInstances(); + } + + private String getInternalName(long accounId) { + String instanceSuffix = configurationDao.getValue(Config.InstanceName.key()); + if (instanceSuffix == null) { + instanceSuffix = "DEFAULT"; + } + long vmId = userVmDao.getNextInSequence(Long.class, "id"); + return VirtualMachineName.getVmName(vmId, accounId, instanceSuffix); + } + @Override public String getConfigComponentName() { return UnmanagedVMsManagerImpl.class.getSimpleName(); @@ -1750,6 +2506,6 @@ public String getConfigComponentName() { @Override public ConfigKey[] getConfigKeys() { - return new ConfigKey[] { UnmanageVMPreserveNic }; + return new ConfigKey[]{UnmanageVMPreserveNic}; } } diff --git a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java index 1a66f1ea9cd7..e78319983531 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -19,8 +19,14 @@ import com.cloud.agent.AgentManager; import com.cloud.agent.api.Answer; +import com.cloud.agent.api.CheckVolumeAnswer; +import com.cloud.agent.api.CheckVolumeCommand; import com.cloud.agent.api.ConvertInstanceAnswer; import com.cloud.agent.api.ConvertInstanceCommand; +import com.cloud.agent.api.CopyRemoteVolumeAnswer; +import com.cloud.agent.api.CopyRemoteVolumeCommand; +import com.cloud.agent.api.GetRemoteVmsAnswer; +import com.cloud.agent.api.GetRemoteVmsCommand; import com.cloud.agent.api.GetUnmanagedInstancesAnswer; import com.cloud.agent.api.GetUnmanagedInstancesCommand; import com.cloud.agent.api.to.DataStoreTO; @@ -32,9 +38,12 @@ import com.cloud.dc.dao.ClusterDao; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.VmwareDatacenterDao; +import com.cloud.deploy.DeployDestination; +import com.cloud.deploy.DeploymentPlanningManager; import com.cloud.event.ActionEventUtils; import com.cloud.event.UsageEventUtils; import com.cloud.exception.AgentUnavailableException; +import com.cloud.exception.InsufficientServerCapacityException; import com.cloud.exception.InvalidParameterValueException; import com.cloud.exception.OperationTimedoutException; import com.cloud.exception.PermissionDeniedException; @@ -50,6 +59,7 @@ import com.cloud.network.NetworkModel; import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkVO; +import com.cloud.offering.NetworkOffering; import com.cloud.offering.ServiceOffering; import com.cloud.resource.ResourceManager; import com.cloud.resource.ResourceState; @@ -59,8 +69,11 @@ import com.cloud.storage.DiskOfferingVO; import com.cloud.storage.ScopeType; import com.cloud.storage.Storage; +import com.cloud.storage.StoragePool; +import com.cloud.storage.StoragePoolHostVO; import com.cloud.storage.VMTemplateStoragePoolVO; import com.cloud.storage.VMTemplateVO; +import com.cloud.storage.Volume; import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; @@ -79,7 +92,9 @@ import com.cloud.user.dao.UserDao; import com.cloud.uservm.UserVm; import com.cloud.utils.Pair; +import com.cloud.utils.db.EntityManager; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.vm.DiskProfile; import com.cloud.vm.NicProfile; import com.cloud.vm.NicVO; import com.cloud.vm.UserVmManager; @@ -96,6 +111,8 @@ 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.ListUnmanagedInstancesCmd; +import org.apache.cloudstack.api.command.admin.vm.ListVmsForImportCmd; +import org.apache.cloudstack.api.response.ListResponse; import org.apache.cloudstack.api.response.UserVmResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -123,10 +140,12 @@ import java.net.URI; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyMap; @@ -148,6 +167,10 @@ public class UnmanagedVMsManagerImplTest { @Mock private ClusterDao clusterDao; @Mock + private ClusterVO clusterVO; + @Mock + private UserVmVO userVm; + @Mock private ResourceManager resourceManager; @Mock private VMTemplatePoolDao templatePoolDao; @@ -212,6 +235,10 @@ public class UnmanagedVMsManagerImplTest { private VMInstanceVO virtualMachine; @Mock private NicVO nicVO; + @Mock + EntityManager entityMgr; + @Mock + DeploymentPlanningManager deploymentPlanningManager; private static final long virtualMachineId = 1L; @@ -275,6 +302,7 @@ public void setUp() throws Exception { hosts.add(hostVO); when(hostVO.checkHostServiceOfferingTags(Mockito.any())).thenReturn(true); when(resourceManager.listHostsInClusterByStatus(Mockito.anyLong(), Mockito.any(Status.class))).thenReturn(hosts); + when(resourceManager.listAllUpAndEnabledHostsInOneZoneByHypervisor(any(Hypervisor.HypervisorType.class), Mockito.anyLong())).thenReturn(hosts); List templates = new ArrayList<>(); when(templatePoolDao.listAll()).thenReturn(templates); List volumes = new ArrayList<>(); @@ -284,6 +312,9 @@ public void setUp() throws Exception { map.put(instance.getName(), instance); Answer answer = new GetUnmanagedInstancesAnswer(cmd, "", map); when(agentManager.easySend(Mockito.anyLong(), Mockito.any(GetUnmanagedInstancesCommand.class))).thenReturn(answer); + GetRemoteVmsCommand remoteVmListcmd = Mockito.mock(GetRemoteVmsCommand.class); + Answer remoteVmListAnswer = new GetRemoteVmsAnswer(remoteVmListcmd, "", map); + when(agentManager.easySend(Mockito.anyLong(), any(GetRemoteVmsCommand.class))).thenReturn(remoteVmListAnswer); DataCenterVO zone = Mockito.mock(DataCenterVO.class); when(zone.getId()).thenReturn(1L); when(dataCenterDao.findById(Mockito.anyLong())).thenReturn(zone); @@ -323,7 +354,7 @@ public void setUp() throws Exception { when(userVmManager.importVM(nullable(DataCenter.class), nullable(Host.class), nullable(VirtualMachineTemplate.class), nullable(String.class), nullable(String.class), nullable(Account.class), nullable(String.class), nullable(Account.class), nullable(Boolean.class), nullable(String.class), nullable(Long.class), nullable(Long.class), nullable(ServiceOffering.class), nullable(String.class), - nullable(String.class), nullable(Hypervisor.HypervisorType.class), nullable(Map.class), nullable(VirtualMachine.PowerState.class))).thenReturn(userVm); + nullable(String.class), nullable(Hypervisor.HypervisorType.class), nullable(Map.class), nullable(VirtualMachine.PowerState.class), nullable(LinkedHashMap.class))).thenReturn(userVm); NetworkVO networkVO = Mockito.mock(NetworkVO.class); when(networkVO.getGuestType()).thenReturn(Network.GuestType.L2); when(networkVO.getBroadcastUri()).thenReturn(URI.create(String.format("vlan://%d", instanceNic.getVlan()))); @@ -426,19 +457,71 @@ public void unmanageVMInstanceExpungedInstanceTest() { @Test(expected = UnsupportedServiceException.class) public void unmanageVMInstanceExistingVMSnapshotsTest() { + when(virtualMachine.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.None); unmanagedVMsManager.unmanageVMInstance(virtualMachineId); } @Test(expected = UnsupportedServiceException.class) public void unmanageVMInstanceExistingVolumeSnapshotsTest() { + when(virtualMachine.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.None); unmanagedVMsManager.unmanageVMInstance(virtualMachineId); } @Test(expected = UnsupportedServiceException.class) public void unmanageVMInstanceExistingISOAttachedTest() { + when(virtualMachine.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.None); unmanagedVMsManager.unmanageVMInstance(virtualMachineId); } + @Test + public void testListRemoteInstancesTest() { + ListVmsForImportCmd cmd = Mockito.mock(ListVmsForImportCmd.class); + when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString()); + when(cmd.getUsername()).thenReturn("user"); + when(cmd.getPassword()).thenReturn("pass"); + ListResponse response = unmanagedVMsManager.listVmsForImport(cmd); + Assert.assertEquals(1, response.getCount().intValue()); + } + + @Test(expected = InvalidParameterValueException.class) + public void testListRemoteInstancesTestNonKVM() { + ListVmsForImportCmd cmd = Mockito.mock(ListVmsForImportCmd.class); + unmanagedVMsManager.listVmsForImport(cmd); + } + @Test + public void testImportFromExternalTest() throws InsufficientServerCapacityException { + String vmname = "TestInstance"; + ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); + when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString()); + when(cmd.getName()).thenReturn(vmname); + when(cmd.getUsername()).thenReturn("user"); + when(cmd.getPassword()).thenReturn("pass"); + when(cmd.getImportSource()).thenReturn("external"); + when(cmd.getDomainId()).thenReturn(null); + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(templateDao.findByName(anyString())).thenReturn(template); + HostVO host = Mockito.mock(HostVO.class); + when(userVmDao.getNextInSequence(Long.class, "id")).thenReturn(1L); + DeployDestination mockDest = Mockito.mock(DeployDestination.class); + when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); + DiskProfile diskProfile = Mockito.mock(DiskProfile.class); + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(diskProfile); + Map storage = new HashMap<>(); + VolumeVO volume = Mockito.mock(VolumeVO.class); + StoragePoolVO storagePool = Mockito.mock(StoragePoolVO.class); + storage.put(volume, storagePool); + when(mockDest.getStorageForDisks()).thenReturn(storage); + when(mockDest.getHost()).thenReturn(host); + when(volumeDao.findById(anyLong())).thenReturn(volume); + CopyRemoteVolumeAnswer copyAnswer = Mockito.mock(CopyRemoteVolumeAnswer.class); + when(copyAnswer.getResult()).thenReturn(true); + when(agentManager.easySend(anyLong(), any(CopyRemoteVolumeCommand.class))).thenReturn(copyAnswer); + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + unmanagedVMsManager.importVm(cmd); + } + } + private void baseBasicParametersCheckForImportInstance(String name, Long domainId, String accountName) { unmanagedVMsManager.basicParametersCheckForImportInstance(name, domainId, accountName); } @@ -518,7 +601,7 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, when(importVmCmd.getClusterId()).thenReturn(clusterId); when(importVmCmd.getDomainId()).thenReturn(null); when(importVmCmd.getImportSource()).thenReturn(VmImportService.ImportSource.VMWARE.toString()); - when(importVmCmd.getHost()).thenReturn(host); + when(importVmCmd.getHostIp()).thenReturn(host); when(importVmCmd.getNicNetworkList()).thenReturn(Map.of("NIC 1", networkId)); when(importVmCmd.getConvertInstanceHostId()).thenReturn(null); when(importVmCmd.getConvertStoragePoolId()).thenReturn(null); @@ -544,9 +627,6 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, if (selectConvertHost) { when(importVmCmd.getConvertInstanceHostId()).thenReturn(convertHostId); when(hostDao.findById(convertHostId)).thenReturn(convertHost); - } else { - when(hostDao.listByClusterAndHypervisorType(clusterId, Hypervisor.HypervisorType.KVM)) - .thenReturn(List.of(convertHost)); } DataStoreTO dataStoreTO = mock(DataStoreTO.class); @@ -613,6 +693,57 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, } @Test + public void testImportFromLocalDisk() throws InsufficientServerCapacityException { + testImportFromDisk("local"); + } + + @Test + public void testImportFromsharedStorage() throws InsufficientServerCapacityException { + testImportFromDisk("shared"); + } + + private void testImportFromDisk(String source) throws InsufficientServerCapacityException { + String vmname = "testVm"; + ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); + when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString()); + when(cmd.getName()).thenReturn(vmname); + when(cmd.getImportSource()).thenReturn(source); + when(cmd.getDiskPath()).thenReturn("/var/lib/libvirt/images/test.qcow2"); + when(cmd.getDomainId()).thenReturn(null); + VMTemplateVO template = Mockito.mock(VMTemplateVO.class); + when(templateDao.findByName(anyString())).thenReturn(template); + HostVO host = Mockito.mock(HostVO.class); + when(hostDao.findById(anyLong())).thenReturn(host); + NetworkOffering netOffering = Mockito.mock(NetworkOffering.class); + when(entityMgr.findById(NetworkOffering.class, 0L)).thenReturn(netOffering); + when(userVmDao.getNextInSequence(Long.class, "id")).thenReturn(1L); + DeployDestination mockDest = Mockito.mock(DeployDestination.class); + when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); + DiskProfile diskProfile = Mockito.mock(DiskProfile.class); + when(volumeManager.allocateRawVolume(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(diskProfile); + Map storage = new HashMap<>(); + VolumeVO volume = Mockito.mock(VolumeVO.class); + StoragePoolVO storagePool = Mockito.mock(StoragePoolVO.class); + storage.put(volume, storagePool); + when(mockDest.getStorageForDisks()).thenReturn(storage); + when(mockDest.getHost()).thenReturn(host); + when(volumeDao.findById(anyLong())).thenReturn(volume); + CheckVolumeAnswer answer = Mockito.mock(CheckVolumeAnswer.class); + when(answer.getResult()).thenReturn(true); + when(agentManager.easySend(anyLong(), any(CheckVolumeCommand.class))).thenReturn(answer); + List storagePools = new ArrayList<>(); + storagePools.add(storagePool); + when(primaryDataStoreDao.findLocalStoragePoolsByHostAndTags(anyLong(), any())).thenReturn(storagePools); + when(primaryDataStoreDao.findById(anyLong())).thenReturn(storagePool); + when(volumeApiService.doesTargetStorageSupportDiskOffering(any(StoragePool.class), any())).thenReturn(true); + StoragePoolHostVO storagePoolHost = Mockito.mock(StoragePoolHostVO.class); + when(storagePoolHostDao.findByPoolHost(anyLong(), anyLong())).thenReturn(storagePoolHost); + try (MockedStatic ignored = Mockito.mockStatic(UsageEventUtils.class)) { + unmanagedVMsManager.importVm(cmd); + } + } + public void testImportVmFromVmwareToKvmExistingVcenter() throws OperationTimedoutException, AgentUnavailableException { baseTestImportVmFromVmwareToKvm(VcenterParameter.EXISTING, false, false); } diff --git a/tools/apidoc/gen_toc.py b/tools/apidoc/gen_toc.py index 627d569123b9..a2417bdac927 100644 --- a/tools/apidoc/gen_toc.py +++ b/tools/apidoc/gen_toc.py @@ -282,7 +282,9 @@ 'createBucket': 'Object Store', 'updateBucket': 'Object Store', 'deleteBucket': 'Object Store', - 'listBuckets': 'Object Store' + 'listBuckets': 'Object Store', + 'listVmsForImport': 'Virtual Machine', + 'importVm': 'Virtual Machine' } diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index de7e3d372d2b..c4630338db8e 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -158,6 +158,7 @@ "label.action.image.store.read.only": "Make image store read-only", "label.action.image.store.read.write": "Make image store read-write", "label.action.import.export.instances": "Import-Export Instances", +"label.action.ingest.instances": "Ingest instances", "label.action.iso.permission": "Update ISO permissions", "label.action.iso.share": "Update ISO sharing", "label.action.lock.account": "Lock Account", @@ -694,7 +695,11 @@ "label.deployasis": "Read Instance settings from OVA", "label.deploymentplanner": "Deployment planner", "label.desc.db.stats": "Database Statistics", -"label.desc.importexportinstancewizard": "Import and export Instances to/from an existing VMware cluster.", +"label.desc.importexportinstancewizard": "Import and export Instances to/from an existing VMware or KVM cluster.", +"label.desc.import.ext.kvm.wizard": "Import libvirt domain from KVM Host", +"label.desc.import.local.kvm.wizard": "Import QCOW image from Local Storage", +"label.desc.import.shared.kvm.wizard": "Import QCOW image from Shared Storage", +"label.desc.ingesttinstancewizard": "Ingest instances from an external KVM host", "label.desc.importmigratefromvmwarewizard": "Import instances from VMware into a KVM cluster", "label.desc.usage.stats": "Usage Server Statistics", "label.description": "Description", @@ -704,10 +709,10 @@ "label.desthost": "Destination host", "label.destination": "Destination", "label.destination.cluster": "Destination Cluster", -"label.destination.hypervisor": "Destination Hypervisor", "label.destination.pod": "Destination Pod", "label.destination.zone": "Destination Zone", "label.destinationphysicalnetworkid": "Destination physical Network ID", +"label.destination.hypervisor": "Destination Hypervisor", "label.destinationtype": "Destination Type", "label.destipprefix": "Destination Network Address", "label.destipprefixlen": "Destination Prefix Length", @@ -738,6 +743,8 @@ "label.disconnected": "Last disconnected", "label.disk": "Disk", "label.disk.offerings": "Disk offerings", +"label.disk.path": "Disk Path", +"label.disk.tooltip": "Disk Image filename in the selected Storage Pool", "label.disk.selection": "Disk selection", "label.disk.size": "Disk size", "label.disk.volume": "Disk volume", @@ -888,6 +895,7 @@ "label.expunged": "Expunged", "label.expunging": "Expunging", "label.export.rules": "Export Rules", +"label.ext.hostname.tooltip": "External Host Name or IP Address", "label.external.managed": "ExternalManaged", "label.external": "External", "label.external.link": "External link", @@ -898,6 +906,7 @@ "label.f5.ip.loadbalancer": "F5 BIG-IP load balancer.", "label.failed": "Failed", "label.featured": "Featured", +"label.fetch.instances": "Fetch Instances", "label.fetch.latest": "Fetch latest", "label.files": "Alternate files to retrieve", "label.filter": "Filter", @@ -1005,6 +1014,7 @@ "label.hostcontrolstate": "Control Plane Status", "label.hostid": "Host", "label.hostname": "Host", +"label.hostname.tooltip": "Destination Host. Volume should be located in local storage of this Host", "label.hostnamelabel": "Host name", "label.hosts": "Hosts", "label.hosttags": "Host tags", @@ -1041,6 +1051,7 @@ "label.info": "Info", "label.info.upper": "INFO", "label.infrastructure": "Infrastructure", +"label.ingest.instance": "Ingest Instance", "label.ingress": "Ingress", "label.ingress.rule": "Ingress Rule", "label.initial": "Inital", @@ -1552,6 +1563,7 @@ "label.password": "Password", "label.password.default": "Default Password", "label.password.reset.confirm": "Password has been reset to ", +"label.password.tooltip": "The password for the Host", "label.passwordenabled": "Password enabled", "label.path": "Path", "label.patp": "Palo Alto threat profile", @@ -1723,6 +1735,7 @@ "label.release.dedicated.pod": "Release dedicated pod", "label.release.dedicated.zone": "Release dedicated zone", "label.releasing.ip": "Releasing IP", +"label.remote.instances": "Remote Instances", "label.remove": "Remove", "label.remove.annotation": "Remove comment", "label.remove.egress.rule": "Remove egress rule", @@ -1847,6 +1860,7 @@ "label.scheduled.snapshots": "Scheduled Snapshots", "label.schedules": "Schedules", "label.scope": "Scope", +"label.scope.tooltip": "Primary Storage Pool Scope", "label.search": "Search", "label.secondary.isolated.vlan.type.isolated": "Isolated", "label.secondary.isolated.vlan.type.promiscuous": "Promiscuous", @@ -1877,6 +1891,7 @@ "label.select.project": "Select project", "label.select.projects": "Select projects", "label.select.ps": "Select primary storage", +"label.select.root.disk": "Select the ROOT disk", "label.select.source.vcenter.datacenter": "Select the source VMware vCenter Datacenter", "label.select.tier": "Select Network Tier", "label.select.zones": "Select zones", @@ -2033,6 +2048,7 @@ "label.storagemotionenabled": "Storage motion enabled", "label.storagepolicy": "Storage policy", "label.storagepool": "Storage pool", +"label.storagepool.tooltip": "Destination Storage Pool. Volume should be located in this Storage Pool", "label.storagetags": "Storage tags", "label.storagetype": "Storage type", "label.strict": "Strict", @@ -2122,6 +2138,8 @@ "label.timeout": "Timeout", "label.timeout.in.second ": " Timeout (seconds)", "label.timezone": "Timezone", +"label.tmppath": "Temp Path", +"label.tmppath.tooltip": "Temporary Path to store disk images on External Host before copying to destination storage pool. Default is /tmp", "label.to": "to", "label.token": "Token", "label.token.for.dashboard.login": "Token for dashboard login can be retrieved using following command", @@ -2228,6 +2246,7 @@ "label.tpm": "TPM enabled", "label.userdatal2": "User data", "label.username": "Username", +"label.username.tooltip": "The Username for the Host", "label.users": "Users", "label.usersource": "User type", "label.using.cli": "Using CLI", @@ -2751,6 +2770,10 @@ "message.desc.create.ssh.key.pair": "Please fill in the following data to create or register a ssh key pair.

(1) If public key is set, CloudStack will register the public key. You can use it through your private key.

(2) If public key is not set, CloudStack will create a new SSH key pair. In this case, please copy and save the private key. CloudStack will not keep it.
", "message.desc.created.ssh.key.pair": "Created a SSH key pair.", "message.desc.host": "Each cluster must contain at least one host (computer) for guest Instances to run on. We will add the first host now. For a host to function in CloudStack, you must install hypervisor software on the host, assign an IP address to the host, and ensure the host is connected to the CloudStack management server.

Give the host's DNS or IP address, the user name (usually root) and password, and any labels you use to categorize hosts.", +"message.desc.importingestinstancewizard": "This feature only applies to libvirt based KVM instances. Only Stopped instances can be ingested", +"message.desc.import.ext.kvm.wizard": "Import libvirt domain from External KVM Host not managed by CloudStack", +"message.desc.import.local.kvm.wizard": "Import QCOW image from Local Storage of selected KVM Host", +"message.desc.import.shared.kvm.wizard": "Import QCOW image from selected Primary Storage Pool", "message.desc.importexportinstancewizard": "By choosing to manage an Instance, CloudStack takes over the orchestration of that Instance. Unmanaging an Instance removes CloudStack ability to manage it. In both cases, the Instance is left running and no changes are done to the VM on the hypervisor.

For KVM, managing a VM is an experimental feature.", "message.desc.importmigratefromvmwarewizard": "By selecting an existing or external VMware Datacenter and an instance to import, CloudStack migrates the selected instance from VMware to KVM on a conversion host using virt-v2v and imports it into a KVM cluster", "message.desc.primary.storage": "Each cluster must contain one or more primary storage servers. We will add the first one now. Primary storage contains the disk volumes for all the Instances running on hosts in the cluster. Use any standards-compliant protocol that is supported by the underlying hypervisor.", diff --git a/ui/src/config/section/compute.js b/ui/src/config/section/compute.js index f558c473ff1e..722e7891b432 100644 --- a/ui/src/config/section/compute.js +++ b/ui/src/config/section/compute.js @@ -446,7 +446,7 @@ export default { label: 'label.action.unmanage.virtualmachine', message: 'message.action.unmanage.virtualmachine', dataView: true, - show: (record) => { return ['Running', 'Stopped'].includes(record.state) && record.hypervisor === 'VMware' } + show: (record) => { return ['Running', 'Stopped'].includes(record.state) && ['VMware', 'KVM'].includes(record.hypervisor) } }, { api: 'expungeVirtualMachine', diff --git a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue index 2887a415e57c..4450ce1144cd 100644 --- a/ui/src/views/compute/wizard/ComputeOfferingSelection.vue +++ b/ui/src/views/compute/wizard/ComputeOfferingSelection.vue @@ -173,7 +173,7 @@ export default { disabled = true } if (disabled === false && maxMemory && this.minimumMemory > 0 && - ((item.iscustomized === false && maxMemory < this.minimumMemory) || + ((item.iscustomized === false && ((maxMemory < this.minimumMemory) || this.exactMatch && maxMemory !== this.minimumMemory)) || (item.iscustomized === true && maxMemory < this.minimumMemory))) { disabled = true } diff --git a/ui/src/views/compute/wizard/MultiDiskSelection.vue b/ui/src/views/compute/wizard/MultiDiskSelection.vue index 5dd8466279f4..8344508ad33c 100644 --- a/ui/src/views/compute/wizard/MultiDiskSelection.vue +++ b/ui/src/views/compute/wizard/MultiDiskSelection.vue @@ -31,7 +31,7 @@ {{ record.displaytext || record.name }}
- {{ meta.key + ': ' + meta.value }} + {{ meta.key + ': ' + meta.value }}
@@ -104,6 +104,10 @@ export default { autoSelectLabel: { type: String, default: '' + }, + isKVMUnmanage: { + type: Boolean, + default: false } }, data () { diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue index 86decf75d571..6433e68da575 100644 --- a/ui/src/views/tools/ImportUnmanagedInstance.vue +++ b/ui/src/views/tools/ImportUnmanagedInstance.vue @@ -19,7 +19,7 @@
- + - + @@ -120,7 +120,7 @@ :value="templateType" @change="changeTemplateType"> - + {{ $t('label.template.temporary.import') }} @@ -235,7 +235,7 @@ - + + @change="onSelectRootDisk"> {{ opt.label || opt.id }} + + +
@@ -283,17 +299,40 @@ :zoneId="cluster.zoneid" :selectionEnabled="false" :filterUnimplementedNetworks="true" - filterMatchKey="broadcasturi" :hypervisor="this.cluster.hypervisortype" + :filterMatchKey="isKVMUnmanage ? undefined : 'broadcasturi'" @select-multi-network="updateMultiNetworkOffering" /> - - - - + +
+ + + +
+
+ + + + + + {{ network.label }} + + + + +
@@ -358,6 +397,26 @@ export default { type: Object, required: true }, + host: { + type: Object, + required: true + }, + pool: { + type: Object, + required: true + }, + resource: { + type: Object, + required: true + }, + isOpen: { + type: Boolean, + required: false + }, + zoneid: { + type: String, + required: false + }, importsource: { type: String, required: false @@ -366,12 +425,24 @@ export default { type: String, required: false }, - resource: { - type: Object, - required: true + exthost: { + type: String, + required: false }, - isOpen: { - type: Boolean, + username: { + type: String, + required: false + }, + password: { + type: String, + required: false + }, + tmppath: { + type: String, + required: false + }, + diskpath: { + type: String, required: false }, selectedVmwareVcenter: { @@ -384,12 +455,14 @@ export default { options: { domains: [], projects: [], + networks: [], templates: [] }, rowCount: {}, optionsLoading: { domains: false, projects: false, + networks: false, templates: false }, domains: [], @@ -397,7 +470,7 @@ export default { selectedDomainId: null, templates: [], templateLoading: false, - templateType: 'auto', + templateType: this.defaultTemplateType(), totalComputeOfferings: 0, computeOfferings: [], computeOfferingLoading: false, @@ -426,7 +499,15 @@ export default { storagePoolsForConversion: [], selectedStorageOptionForConversion: null, selectedStoragePoolForConversion: null, - showStoragePoolsForConversion: false + showStoragePoolsForConversion: false, + selectedRootDiskColumns: [ + { + key: 'name', + dataIndex: 'name', + title: this.$t('label.rootdisk') + } + ], + selectedRootDiskSources: [] } }, beforeCreate () { @@ -461,6 +542,15 @@ export default { showicon: true } }, + networks: { + list: 'listNetworks', + isLoad: true, + field: 'networkid', + options: { + zoneid: this.zoneid, + details: 'min' + } + }, templates: { list: 'listTemplates', isLoad: true, @@ -479,6 +569,21 @@ export default { } return false }, + isDiskImport () { + if (this.importsource === 'local' || this.importsource === 'shared') { + return true + } + return false + }, + isExternalImport () { + if (this.importsource === 'external') { + return true + } + return false + }, + isKVMUnmanage () { + return this.hypervisor && this.hypervisor === 'kvm' && (this.importsource === 'unmanaged' || this.importsource === 'external') + }, domainSelectOptions () { var domains = this.options.domains.map((domain) => { return { @@ -507,6 +612,19 @@ export default { }) return projects }, + networkSelectOptions () { + var networks = this.options.networks.map((network) => { + return { + label: network.name + ' (' + network.displaytext + ')', + value: network.id + } + }) + networks.unshift({ + label: '', + value: null + }) + return networks + }, templateSelectOptions () { return this.options.templates.map((template) => { return { @@ -540,6 +658,9 @@ export default { var nic = { ...nicEntry } nic.name = nic.name || nic.id nic.displaytext = nic.name + if (this.isExternalImport && nic.vlanid === -1) { + delete nic.vlanid + } if (nic.vlanid) { nic.broadcasturi = 'vlan://' + nic.vlanid if (nic.isolatedpvlan) { @@ -592,6 +713,9 @@ export default { page: 1 }) this.fetchKvmHostsForConversion() + if (this.resource.disk.length > 1) { + this.updateSelectedRootDisk() + } }, getMeta (obj, metaKeys) { var meta = [] @@ -724,6 +848,12 @@ export default { updateMultiNetworkOffering (data) { this.nicsNetworksMapping = data }, + defaultTemplateType () { + if (this.cluster.hypervisortype === 'VMWare') { + return 'auto' + } + return 'custom' + }, changeTemplateType (e) { this.templateType = e.target.value if (this.templateType === 'auto') { @@ -834,6 +964,17 @@ export default { } ] }, + onSelectRootDisk (val) { + this.selectedRootDiskIndex = val + this.updateSelectedRootDisk() + }, + updateSelectedRootDisk () { + var rootDisk = this.resource.disk[this.selectedRootDiskIndex] + rootDisk.size = rootDisk.capacity / (1024 * 1024 * 1024) + rootDisk.name = `${rootDisk.label} (${rootDisk.size} GB)` + rootDisk.meta = this.getMeta(rootDisk, { controller: 'controller', datastorename: 'datastore', position: 'position' }) + this.selectedRootDiskSources = [rootDisk] + }, handleSubmit (e) { e.preventDefault() if (this.loading) return @@ -843,12 +984,32 @@ export default { name: this.resource.name, clusterid: this.cluster.id, displayname: values.displayname, + zoneid: this.zoneid, importsource: this.importsource, - hypervisor: this.hypervisor + hypervisor: this.hypervisor, + host: this.exthost, + hostname: values.hostname, + username: this.username, + password: this.password, + hostid: this.host.id, + storageid: this.pool.id, + diskpath: this.diskpath, + temppath: this.tmppath } var importapi = 'importUnmanagedInstance' if (this.isExternalImport || this.isDiskImport || this.selectedVmwareVcenter) { importapi = 'importVm' + if (this.isDiskImport) { + if (!values.networkid) { + this.$notification.error({ + message: this.$t('message.request.failed'), + description: this.$t('message.please.enter.valid.value') + ': ' + this.$t('label.network') + }) + return + } + params.name = values.displayname + params.networkid = values.networkid + } } if (!this.computeOffering || !this.computeOffering.id) { this.$notification.error({ @@ -892,6 +1053,16 @@ export default { }) } } + if (this.isDiskImport) { + var storageType = this.computeOffering.storagetype + if (this.importsource !== storageType) { + this.$notification.error({ + message: this.$t('message.request.failed'), + description: 'Incompatible Storage. Import Source is: ' + this.importsource + '. Storage Type in service offering is: ' + storageType + }) + return + } + } if (this.selectedVmwareVcenter) { if (this.selectedVmwareVcenter.existingvcenterid) { params.existingvcenterid = this.selectedVmwareVcenter.existingvcenterid @@ -934,6 +1105,7 @@ export default { } var nicNetworkIndex = 0 var nicIpIndex = 0 + var networkcheck = new Set() for (var nicId in this.nicsNetworksMapping) { if (!this.nicsNetworksMapping[nicId].network) { this.$notification.error({ @@ -944,6 +1116,16 @@ export default { } params['nicnetworklist[' + nicNetworkIndex + '].nic'] = nicId params['nicnetworklist[' + nicNetworkIndex + '].network'] = this.nicsNetworksMapping[nicId].network + var netId = this.nicsNetworksMapping[nicId].network + if (!networkcheck.has(netId)) { + networkcheck.add(netId) + } else { + this.$notification.error({ + message: this.$t('message.request.failed'), + description: 'Same network cannot be assigned to multiple Nics' + }) + return + } nicNetworkIndex++ if ('ipAddress' in this.nicsNetworksMapping[nicId]) { if (!this.nicsNetworksMapping[nicId].ipAddress) { @@ -1010,7 +1192,7 @@ export default { for (var field of fields) { this.updateFieldValue(field, undefined) } - this.templateType = 'auto' + this.templateType = this.defaultTemplateType() this.updateComputeOffering(undefined) this.switches = {} }, @@ -1022,33 +1204,33 @@ export default { diff --git a/ui/src/views/tools/ManageInstances.vue b/ui/src/views/tools/ManageInstances.vue index 96eba5396388..fc14f684e724 100644 --- a/ui/src/views/tools/ManageInstances.vue +++ b/ui/src/views/tools/ManageInstances.vue @@ -48,126 +48,278 @@
- - - - - - - VMware - - - KVM - - - - + + + + + + + + VMware + + + KVM + + + + + + + + {{ opt.label }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('label.clusterid') }} + {{ $t('label.zoneid') }} + + + - + - {{ opt.label }} + + + {{ zoneitem.label }} - - - - - - - - - - - - - - - {{ zoneitem.label }} - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + {{ $t('label.import.instance') }} + + + + + + + + +
+ + {{ $t('label.fetch.instances') }} + +
- + + + +