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 3284dbafe7ca..8f28ef3f5f29 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 @@ -32,6 +32,7 @@ import org.apache.cloudstack.api.ServerApiException; import org.apache.cloudstack.api.response.ClusterResponse; import org.apache.cloudstack.api.response.DomainResponse; +import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.ProjectResponse; import org.apache.cloudstack.api.response.ServiceOfferingResponse; import org.apache.cloudstack.api.response.TemplateResponse; @@ -118,6 +119,13 @@ public class ImportUnmanagedInstanceCmd extends BaseAsyncCmd { description = "The ID of the Template for the Instance") private Long templateId; + @Parameter(name = ApiConstants.OS_ID, + type = CommandType.UUID, + entityType = GuestOSResponse.class, + since = "4.23.0", + description = "optional - the ID of the guest OS for the imported Instance.") + private Long guestOsId; + @Parameter(name = ApiConstants.SERVICE_OFFERING_ID, type = CommandType.UUID, entityType = ServiceOfferingResponse.class, @@ -187,6 +195,10 @@ public Long getTemplateId() { return templateId; } + public Long getGuestOsId() { + return guestOsId; + } + public Long getProjectId() { return projectId; } 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 db7dcc3fb44f..6d1d0cc3f7f6 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 @@ -30,7 +30,6 @@ import org.apache.cloudstack.api.Parameter; import org.apache.cloudstack.api.ResponseObject; import org.apache.cloudstack.api.ServerApiException; -import org.apache.cloudstack.api.response.GuestOSResponse; import org.apache.cloudstack.api.response.HostResponse; import org.apache.cloudstack.api.response.NetworkResponse; import org.apache.cloudstack.api.response.StoragePoolResponse; @@ -107,6 +106,18 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { description = "the network ID") private Long networkId; + @Parameter(name = ApiConstants.MAC_ADDRESS, + type = CommandType.STRING, + since = "4.23.0", + description = "(only for importing VMs from KVM local/shared storage) optional - the MAC address for the imported VM NIC. If omitted, a new MAC address is generated.") + private String macAddress; + + @Parameter(name = ApiConstants.IP_ADDRESS, + type = CommandType.STRING, + since = "4.22.1", + description = "(only for importing VMs from KVM local/shared storage) optional - the IPv4 address for the imported VM NIC. If omitted, IPv4 assignment remains automatic.") + private String ipAddress; + @Parameter(name = ApiConstants.HOST_ID, type = CommandType.UUID, entityType = HostResponse.class, description = "Host where local disk is located") private Long hostId; @@ -172,13 +183,6 @@ public class ImportVmCmd extends ImportUnmanagedInstanceCmd { description = "(only for importing VMs from VMware to KVM) optional - if true, forces virt-v2v conversions to write directly on the provided storage pool (avoid using temporary conversion pool).") private Boolean forceConvertToPool; - @Parameter(name = ApiConstants.OS_ID, - type = CommandType.UUID, - entityType = GuestOSResponse.class, - since = "4.22.1", - description = "(only for importing VMs from VMware to KVM) optional - the ID of the guest OS for the imported VM.") - private Long guestOsId; - @Parameter(name = ApiConstants.USE_VDDK, type = CommandType.BOOLEAN, since = "4.22.1", @@ -275,6 +279,14 @@ public Long getNetworkId() { return networkId; } + public String getMacAddress() { + return macAddress; + } + + public String getIpAddress() { + return ipAddress; + } + @Override public String getEventType() { return EventTypes.EVENT_VM_IMPORT; @@ -288,10 +300,6 @@ public boolean getForceConvertToPool() { return BooleanUtils.toBooleanDefaultIfNull(forceConvertToPool, false); } - public Long getGuestOsId() { - return guestOsId; - } - @Override public String getEventDescription() { String vmName = getName(); 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 4d823783a99a..47279e857c48 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 @@ -61,6 +61,7 @@ public class LibvirtDomainXMLParser { private Integer vncPort; private String vncPasswd; private String desc; + private String osInfoId; private LibvirtVMDef.CpuTuneDef cpuTuneDef; private LibvirtVMDef.CpuModeDef cpuModeDef; private String name; @@ -79,6 +80,7 @@ public boolean parseDomainXML(String domXML) { Element rootElement = doc.getDocumentElement(); desc = getTagValue("description", rootElement); + osInfoId = getOsInfoId(rootElement); name = getTagValue("name", rootElement); Element devices = (Element)rootElement.getElementsByTagName("devices").item(0); @@ -455,6 +457,27 @@ private static String getAttrValue(String tag, String attr, Element eElement) { return node.getAttribute(attr); } + private static String getOsInfoId(Element rootElement) { + if (rootElement == null) { + return null; + } + + NodeList nodes = rootElement.getElementsByTagName("*"); + for (int i = 0; i < nodes.getLength(); i++) { + Node node = nodes.item(i); + String nodeName = node.getNodeName(); + String namespace = node.getNamespaceURI(); + if ("libosinfo:os".equals(nodeName) || ("os".equals(node.getLocalName()) && StringUtils.contains(namespace, "libosinfo"))) { + Element element = (Element)node; + String id = element.getAttribute("id"); + if (StringUtils.isNotBlank(id)) { + return id; + } + } + } + return null; + } + /** * Parse the disk block part of the libvirt XML. * @param def @@ -510,6 +533,10 @@ public String getDescription() { return desc; } + public String getOsInfoId() { + return osInfoId; + } + public String getName() { return name; } 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 index c0887415c650..8807db0e01bd 100644 --- 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 @@ -98,6 +98,8 @@ private UnmanagedInstanceTO getUnmanagedInstance(LibvirtComputingResource libvir if (parser.getCpuTuneDef() !=null) { instance.setCpuSpeed(parser.getCpuTuneDef().getShares()); } + instance.setOperatingSystemId(parser.getOsInfoId()); + instance.setOperatingSystem(parser.getDescription()); instance.setHypervisorType(Hypervisor.HypervisorType.KVM.name()); instance.setPowerState(getPowerState(libvirtComputingResource.getVmState(conn,domain.getName()))); instance.setNics(getUnmanagedInstanceNics(parser.getInterfaces())); 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 index 60bc222594bb..53acd2c1537d 100644 --- 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 @@ -131,6 +131,8 @@ private UnmanagedInstanceTO getUnmanagedInstance(LibvirtComputingResource libvir if (parser.getCpuModeDef() != null) { instance.setCpuCoresPerSocket(parser.getCpuModeDef().getCoresPerSocket()); } + instance.setOperatingSystemId(parser.getOsInfoId()); + instance.setOperatingSystem(parser.getDescription()); instance.setHypervisorType(Hypervisor.HypervisorType.KVM.name()); instance.setPowerState(getPowerState(libvirtComputingResource.getVmState(conn,domain.getName()))); instance.setMemory((int) LibvirtComputingResource.getDomainMemory(domain) / 1024); diff --git a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParserTest.java b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParserTest.java index e73b40798668..5649b8c86488 100644 --- a/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParserTest.java +++ b/plugins/hypervisors/kvm/src/test/java/com/cloud/hypervisor/kvm/resource/LibvirtDomainXMLParserTest.java @@ -73,10 +73,17 @@ public void testDomainXMLParser() { String diskPath2 = "/var/lib/libvirt/images/my-test-image2.qcow2"; String secretUuid = "5644d664-a238-3a9b-811c-961f609d29f4"; - String xml = "" + + String osInfoId = "http://ubuntu.com/ubuntu/24.04"; + + String xml = "" + "s-2970-VM" + "4d2c1526-865d-4fc9-a1ac-dbd1801a22d0" + "Debian GNU/Linux 6(64-bit)" + + "" + + "" + + "" + + "" + + "" + "262144" + "262144" + "1" + @@ -220,6 +227,7 @@ public void testDomainXMLParser() { parser.parseDomainXML(xml); assertEquals(vncPort - 5900, (int)parser.getVncPort()); + assertEquals(osInfoId, parser.getOsInfoId()); List disks = parser.getDisks(); /* Disk 0 is the first disk, the QCOW2 file backed virto disk */ 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 846eab599fd1..7f325274101d 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1319,6 +1319,7 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { final Map dataDiskOfferingMap = cmd.getDataDiskToDiskOfferingList(); final Map details = cmd.getDetails(); final boolean forced = cmd.isForced(); + Long guestOsId = cmd.getGuestOsId(); List hosts = resourceManager.listHostsInClusterByStatus(clusterId, Status.Up); UserVm userVm = null; List additionalNameFilters = getAdditionalNameFilters(cluster); @@ -1350,7 +1351,7 @@ private UserVmResponse baseImportInstance(ImportUnmanagedInstanceCmd cmd) { template, instanceName, displayName, hostName, caller, owner, userId, serviceOffering, dataDiskOfferingMap, nicNetworkMap, nicIpAddressMap, - details, cmd.getMigrateAllowed(), managedVms, forced); + details, cmd.getMigrateAllowed(), managedVms, forced, guestOsId); } } @@ -1497,7 +1498,7 @@ private UserVm importUnmanagedInstanceFromHypervisor(DataCenter zone, Cluster cl String hostName, Account caller, Account owner, long userId, ServiceOfferingVO serviceOffering, Map dataDiskOfferingMap, Map nicNetworkMap, Map nicIpAddressMap, - Map details, Boolean migrateAllowed, List managedVms, boolean forced) throws ResourceAllocationException { + Map details, Boolean migrateAllowed, List managedVms, boolean forced, Long guestOsId) throws ResourceAllocationException { UserVm userVm = null; for (HostVO host : hosts) { HashMap unmanagedInstances = getUnmanagedInstancesForHost(host, instanceName, managedVms); @@ -1548,7 +1549,7 @@ private UserVm importUnmanagedInstanceFromHypervisor(DataCenter zone, Cluster cl userVm = importVirtualMachineInternal(unmanagedInstance, instanceName, zone, cluster, host, template, displayName, hostName, CallContext.current().getCallingAccount(), owner, userId, serviceOffering, dataDiskOfferingMap, - nicNetworkMap, nicIpAddressMap, null, + nicNetworkMap, nicIpAddressMap, guestOsId, details, migrateAllowed, forced, true); } finally { ReservationHelper.closeAll(reservations); @@ -2611,6 +2612,9 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { Long hostId = cmd.getHostId(); Long poolId = cmd.getStoragePoolId(); Long networkId = cmd.getNetworkId(); + Long guestOsId = cmd.getGuestOsId(); + String macAddress = cmd.getMacAddress(); + String ipAddress = cmd.getIpAddress(); UnmanagedInstanceTO unmanagedInstanceTO = null; if (ImportSource.EXTERNAL == importSource) { @@ -2679,12 +2683,12 @@ private UserVmResponse importKvmInstance(ImportVmCmd cmd) { userVm = importExternalKvmVirtualMachine(unmanagedInstanceTO, instanceName, zone, template, displayName, hostName, caller, owner, userId, serviceOffering, dataDiskOfferingMap, - nicNetworkMap, nicIpAddressMap, remoteUrl, username, password, tmpPath, details); + nicNetworkMap, nicIpAddressMap, guestOsId, 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, + serviceOffering, dataDiskOfferingMap, networkId, macAddress, ipAddress, guestOsId, hostId, poolId, diskPath, details); } catch (InsufficientCapacityException e) { throw new RuntimeException(e); @@ -2711,7 +2715,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag 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) throws ResourceAllocationException { + final Long guestOsId, final String remoteUrl, String username, String password, String tmpPath, final Map details) throws ResourceAllocationException { UserVm userVm = null; Map allDetails = new HashMap<>(details); @@ -2747,7 +2751,7 @@ private UserVm importExternalKvmVirtualMachine(final UnmanagedInstanceTO unmanag try { userVm = userVmManager.importVM(zone, null, template, null, displayName, owner, null, caller, true, null, owner.getAccountId(), userId, - serviceOffering, null, null, hostName, + serviceOffering, null, guestOsId, hostName, Hypervisor.HypervisorType.KVM, allDetails, powerState, null); } catch (InsufficientCapacityException ice) { logger.error(String.format("Failed to import vm name: %s", instanceName), ice); @@ -2848,6 +2852,7 @@ protected void checkVolumeResourceLimitsForExternalKvmVmImport(Account owner, Un 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 String requestedMacAddress, final String requestedIpAddress, final Long guestOsId, final Long hostId, final Long poolId, final String diskPath, final Map details) throws InsufficientCapacityException, ResourceAllocationException { UserVm userVm = null; @@ -2878,9 +2883,15 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, } } - String macAddress = networkModel.getNextAvailableMacAddressInNetwork(networkId); + String macAddress = StringUtils.defaultIfBlank(requestedMacAddress, networkModel.getNextAvailableMacAddressInNetwork(networkId)); + if (!NetUtils.isValidMac(macAddress)) { + throw new InvalidParameterValueException("MAC address is invalid: " + macAddress); + } - String ipAddress = network.getGuestType() != Network.GuestType.L2 ? "auto" : null; + String ipAddress = StringUtils.defaultIfBlank(requestedIpAddress, network.getGuestType() != Network.GuestType.L2 ? "auto" : null); + if (StringUtils.isNotBlank(ipAddress) && !"auto".equals(ipAddress) && !NetUtils.isValidIp4(ipAddress)) { + throw new InvalidParameterValueException("IP address is invalid: " + ipAddress); + } Network.IpAddresses requestedIpPair = new Network.IpAddresses(ipAddress, null, macAddress); @@ -2903,7 +2914,7 @@ private UserVm importKvmVirtualMachineFromDisk(final ImportSource importSource, checkVmResourceLimitsForExternalKvmVmImport(owner, serviceOffering, (VMTemplateVO) template, details, reservations); userVm = userVmManager.importVM(zone, null, template, null, displayName, owner, null, caller, true, null, owner.getAccountId(), userId, - serviceOffering, null, null, hostName, + serviceOffering, null, guestOsId, hostName, Hypervisor.HypervisorType.KVM, allDetails, powerState, networkNicMap); if (userVm == null) { 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 874bce0f95ef..5dfb55711aa4 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -107,7 +107,6 @@ 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.ResourceAllocationException; @@ -385,6 +384,7 @@ public void setUp() throws Exception { networks.add(networkVO); when(networkDao.listByZone(anyLong())).thenReturn(networks); doNothing().when(networkModel).checkNetworkPermissions(any(Account.class), any(Network.class)); + when(networkModel.getNextAvailableMacAddressInNetwork(anyLong())).thenReturn("02:00:00:00:00:01"); NicProfile profile = Mockito.mock(NicProfile.class); Integer deviceId = 100; Pair pair = new Pair<>(profile, deviceId); @@ -653,7 +653,7 @@ public void testListRemoteInstancesTestNonKVM() { unmanagedVMsManager.listVmsForImport(cmd); } @Test - public void testImportFromExternalTest() throws InsufficientServerCapacityException { + public void testImportFromExternalTest() throws Exception { String vmname = "TestInstance"; ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString()); @@ -662,6 +662,7 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept when(cmd.getPassword()).thenReturn("pass"); when(cmd.getImportSource()).thenReturn("external"); when(cmd.getDomainId()).thenReturn(null); + when(cmd.getGuestOsId()).thenReturn(99L); HostVO host = Mockito.mock(HostVO.class); DeployDestination mockDest = Mockito.mock(DeployDestination.class); when(deploymentPlanningManager.planDeployment(any(), any(), any(), any())).thenReturn(mockDest); @@ -682,6 +683,10 @@ public void testImportFromExternalTest() throws InsufficientServerCapacityExcept MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importVm(cmd); } + verify(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), Mockito.eq(99L), + nullable(String.class), nullable(Hypervisor.HypervisorType.class), nullable(Map.class), nullable(VirtualMachine.PowerState.class), nullable(LinkedHashMap.class)); } private void baseBasicParametersCheckForImportInstance(String name, Long domainId, String accountName) { @@ -943,16 +948,16 @@ private void baseTestImportVmFromVmwareToKvm(VcenterParameter vcenterParameter, } @Test - public void importFromLocalDisk() throws InsufficientServerCapacityException { + public void importFromLocalDisk() throws Exception { importFromDisk("local"); } @Test - public void importFromsharedStorage() throws InsufficientServerCapacityException { + public void importFromsharedStorage() throws Exception { importFromDisk("shared"); } - private void importFromDisk(String source) throws InsufficientServerCapacityException { + private void importFromDisk(String source) throws Exception { String vmname = "testVm"; ImportVmCmd cmd = Mockito.mock(ImportVmCmd.class); when(cmd.getHypervisor()).thenReturn(Hypervisor.HypervisorType.KVM.toString()); @@ -960,6 +965,9 @@ private void importFromDisk(String source) throws InsufficientServerCapacityExce when(cmd.getImportSource()).thenReturn(source); when(cmd.getDiskPath()).thenReturn("/var/lib/libvirt/images/test.qcow2"); when(cmd.getDomainId()).thenReturn(null); + when(cmd.getMacAddress()).thenReturn("02:00:00:00:00:05"); + when(cmd.getIpAddress()).thenReturn("192.0.2.10"); + when(cmd.getGuestOsId()).thenReturn(99L); HostVO host = Mockito.mock(HostVO.class); when(hostDao.findById(anyLong())).thenReturn(host); NetworkOffering netOffering = Mockito.mock(NetworkOffering.class); @@ -990,6 +998,9 @@ private void importFromDisk(String source) throws InsufficientServerCapacityExce MockedConstruction mockCheckedReservation = Mockito.mockConstruction(CheckedReservation.class)) { unmanagedVMsManager.importVm(cmd); } + verify(networkOrchestrationService).importNic(Mockito.eq("02:00:00:00:00:05"), Mockito.eq(0), any(Network.class), Mockito.eq(true), any(VirtualMachine.class), + Mockito.argThat(ipAddresses -> "192.0.2.10".equals(ipAddresses.getIp4Address()) && "02:00:00:00:00:05".equals(ipAddresses.getMacAddress())), + any(DataCenter.class), Mockito.eq(true)); } @Test diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue index ffa0d9344335..2b185c688db2 100644 --- a/ui/src/views/tools/ImportUnmanagedInstance.vue +++ b/ui/src/views/tools/ImportUnmanagedInstance.vue @@ -262,6 +262,24 @@ {{ $t('label.no.matching.guest.os.vmware.import') }} + + + + + {{ ostype.label }} + + +