From d28460f70851609d782f2f5bfc8b7d8c31a854e4 Mon Sep 17 00:00:00 2001 From: andrijapanicsb <45762285+andrijapanicsb@users.noreply.github.com> Date: Thu, 28 May 2026 03:43:12 +0200 Subject: [PATCH 1/2] Improve VMware import guest OS detection --- .../vm/UnmanagedVMsManagerImpl.java | 101 +++++++++++++++++- .../vm/UnmanagedVMsManagerImplTest.java | 90 ++++++++++++++++ ui/src/views/tools/ManageInstances.vue | 89 +++++++++++++-- 3 files changed, 273 insertions(+), 7 deletions(-) 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..845bcb4fd0c7 100644 --- a/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java +++ b/server/src/main/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImpl.java @@ -1732,11 +1732,12 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster Pair sourceInstanceDetails = getSourceVmwareUnmanagedInstance(vcenter, datacenterName, username, password, clusterName, sourceHostName, sourceVMName, serviceOffering); sourceVMwareInstance = sourceInstanceDetails.first(); isClonedInstance = sourceInstanceDetails.second(); + guestOsId = resolveVmwareToKvmImportGuestOsId(guestOsId, sourceVMwareInstance); // Ensure that the configured resource limits will not be exceeded before beginning the conversion process checkVmResourceLimitsForUnmanagedInstanceImport(owner, sourceVMwareInstance, serviceOffering, template, reservations); - boolean isWindowsVm = sourceVMwareInstance.getOperatingSystem().toLowerCase().contains("windows"); + boolean isWindowsVm = StringUtils.containsIgnoreCase(sourceVMwareInstance.getOperatingSystem(), "windows"); if (isWindowsVm) { checkConversionSupportOnHost(convertHost, sourceVMName, true, useVddk, details); } @@ -1792,6 +1793,104 @@ protected UserVm importUnmanagedInstanceFromVmwareToKvm(DataCenter zone, Cluster } } + protected Long resolveVmwareToKvmImportGuestOsId(Long requestedGuestOsId, UnmanagedInstanceTO sourceVMwareInstance) { + if (requestedGuestOsId != null || sourceVMwareInstance == null) { + return requestedGuestOsId; + } + + String osDisplayName = sourceVMwareInstance.getOperatingSystem(); + String osNameForHypervisor = sourceVMwareInstance.getOperatingSystemId(); + String sourceHypervisorVersion = sourceVMwareInstance.getHostHypervisorVersion(); + + GuestOSHypervisor guestOSHypervisor = findVmwareGuestOsMappingByDisplayName(osDisplayName, sourceHypervisorVersion); + if (guestOSHypervisor == null) { + guestOSHypervisor = findVmwareGuestOsMappingByOsName(osNameForHypervisor, sourceHypervisorVersion); + } + if (guestOSHypervisor == null) { + guestOSHypervisor = findVmwareGuestOsMappingByDisplayName(osDisplayName, null); + } + if (guestOSHypervisor == null) { + guestOSHypervisor = findVmwareGuestOsMappingByOsName(osNameForHypervisor, null); + } + if (guestOSHypervisor != null) { + return guestOSHypervisor.getGuestOsId(); + } + + GuestOS guestOS = findVmwareGuestOsByDisplayName(osDisplayName); + return guestOS != null ? guestOS.getId() : null; + } + + private GuestOSHypervisor findVmwareGuestOsMappingByDisplayName(String osDisplayName, String hypervisorVersion) { + List guestOSes = listStrongMatchingGuestOses(osDisplayName); + for (GuestOS guestOS : guestOSes) { + GuestOSHypervisor guestOSHypervisor = guestOSHypervisorDao.findByOsIdAndHypervisor( + guestOS.getId(), Hypervisor.HypervisorType.VMware.toString(), hypervisorVersion); + if (guestOSHypervisor != null) { + return guestOSHypervisor; + } + } + return null; + } + + private GuestOSHypervisor findVmwareGuestOsMappingByOsName(String osNameForHypervisor, String hypervisorVersion) { + if (StringUtils.isBlank(osNameForHypervisor)) { + return null; + } + return guestOSHypervisorDao.findByOsNameAndHypervisor( + osNameForHypervisor, Hypervisor.HypervisorType.VMware.toString(), hypervisorVersion); + } + + private GuestOS findVmwareGuestOsByDisplayName(String osDisplayName) { + List guestOSes = listStrongMatchingGuestOses(osDisplayName); + return CollectionUtils.isNotEmpty(guestOSes) ? guestOSes.get(0) : null; + } + + private List listStrongMatchingGuestOses(String osDisplayName) { + List guestOSes = new ArrayList<>(); + if (StringUtils.isBlank(osDisplayName)) { + return guestOSes; + } + + GuestOS exactMatch = guestOSDao.findOneByDisplayName(osDisplayName); + if (exactMatch != null) { + guestOSes.add(exactMatch); + } + + List candidates = guestOSDao.listLikeDisplayName(osDisplayName); + if (CollectionUtils.isEmpty(candidates)) { + return guestOSes; + } + + for (GuestOS candidate : candidates) { + if (candidate == null || exactMatch != null && candidate.getId() == exactMatch.getId()) { + continue; + } + if (isStrongGuestOsNameMatch(candidate.getDisplayName(), osDisplayName)) { + guestOSes.add(candidate); + } + } + return guestOSes; + } + + private boolean isStrongGuestOsNameMatch(String candidateName, String sourceName) { + String candidate = normalizeGuestOsName(candidateName); + String source = normalizeGuestOsName(sourceName); + if (StringUtils.isAnyBlank(candidate, source)) { + return false; + } + if (candidate.equals(source)) { + return true; + } + if (Math.min(candidate.length(), source.length()) < 8) { + return false; + } + return candidate.contains(source) || source.contains(candidate); + } + + private String normalizeGuestOsName(String name) { + return StringUtils.defaultString(name).toLowerCase().replaceAll("[^a-z0-9]+", " ").trim(); + } + /** * Check whether the conversion storage pool exists and is suitable for the conversion or not. * Secondary storage is only allowed when forceConvertToPool is false. 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 bee6c4ad257f..4e0f38d51d5d 100644 --- a/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java +++ b/server/src/test/java/org/apache/cloudstack/vm/UnmanagedVMsManagerImplTest.java @@ -135,6 +135,8 @@ import com.cloud.service.dao.ServiceOfferingDao; import com.cloud.storage.DataStoreRole; import com.cloud.storage.DiskOfferingVO; +import com.cloud.storage.GuestOSHypervisorVO; +import com.cloud.storage.GuestOSVO; import com.cloud.storage.ScopeType; import com.cloud.storage.Snapshot; import com.cloud.storage.SnapshotVO; @@ -147,6 +149,8 @@ import com.cloud.storage.VolumeApiService; import com.cloud.storage.VolumeVO; import com.cloud.storage.dao.DiskOfferingDao; +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; @@ -249,6 +253,10 @@ public class UnmanagedVMsManagerImplTest { private ImportVmTasksManager importVmTasksManager; @Mock private SnapshotDao snapshotDao; + @Mock + private GuestOSDao guestOSDao; + @Mock + private GuestOSHypervisorDao guestOSHypervisorDao; @Mock private VMInstanceVO virtualMachine; @@ -730,6 +738,88 @@ public void testGetTemplateForImportInstanceDefaultTemplate() { Assert.assertEquals(defaultTemplateName, templateForImportInstance.getName()); } + @Test + public void testResolveVmwareToKvmImportGuestOsIdKeepsRequestedGuestOsId() { + Long resolvedGuestOsId = unmanagedVMsManager.resolveVmwareToKvmImportGuestOsId(1L, instance); + + Assert.assertEquals(Long.valueOf(1L), resolvedGuestOsId); + Mockito.verifyNoInteractions(guestOSDao, guestOSHypervisorDao); + } + + @Test + public void testResolveVmwareToKvmImportGuestOsIdPrefersDisplayNameMapping() { + String osDisplayName = "Debian GNU/Linux 11 (64-bit)"; + instance.setOperatingSystem(osDisplayName); + instance.setOperatingSystemId("otherLinux64Guest"); + instance.setHostHypervisorVersion("8.0.3"); + + GuestOSVO guestOS = mock(GuestOSVO.class); + when(guestOS.getId()).thenReturn(10L); + when(guestOSDao.findOneByDisplayName(osDisplayName)).thenReturn(guestOS); + when(guestOSDao.listLikeDisplayName(osDisplayName)).thenReturn(List.of(guestOS)); + + GuestOSHypervisorVO guestOSHypervisor = mock(GuestOSHypervisorVO.class); + when(guestOSHypervisor.getGuestOsId()).thenReturn(20L); + when(guestOSHypervisorDao.findByOsIdAndHypervisor(10L, Hypervisor.HypervisorType.VMware.toString(), "8.0.3")).thenReturn(guestOSHypervisor); + + Long resolvedGuestOsId = unmanagedVMsManager.resolveVmwareToKvmImportGuestOsId(null, instance); + + Assert.assertEquals(Long.valueOf(20L), resolvedGuestOsId); + Mockito.verify(guestOSHypervisorDao, Mockito.never()).findByOsNameAndHypervisor(anyString(), anyString(), anyString()); + } + + @Test + public void testResolveVmwareToKvmImportGuestOsIdFallsBackToConfiguredGuestOsIdentifier() { + String osDisplayName = "Unmatched Runtime OS"; + instance.setOperatingSystem(osDisplayName); + instance.setOperatingSystemId("ubuntu64Guest"); + instance.setHostHypervisorVersion("8.0.3"); + when(guestOSDao.findOneByDisplayName(osDisplayName)).thenReturn(null); + when(guestOSDao.listLikeDisplayName(osDisplayName)).thenReturn(Collections.emptyList()); + + GuestOSHypervisorVO guestOSHypervisor = mock(GuestOSHypervisorVO.class); + when(guestOSHypervisor.getGuestOsId()).thenReturn(30L); + when(guestOSHypervisorDao.findByOsNameAndHypervisor("ubuntu64Guest", Hypervisor.HypervisorType.VMware.toString(), "8.0.3")).thenReturn(guestOSHypervisor); + + Long resolvedGuestOsId = unmanagedVMsManager.resolveVmwareToKvmImportGuestOsId(null, instance); + + Assert.assertEquals(Long.valueOf(30L), resolvedGuestOsId); + } + + @Test + public void testResolveVmwareToKvmImportGuestOsIdFallsBackToGuestOsTypeMatch() { + String osDisplayName = "Debian GNU/Linux 11 (64-bit)"; + instance.setOperatingSystem(osDisplayName); + instance.setOperatingSystemId("otherLinux64Guest"); + instance.setHostHypervisorVersion("8.0.3"); + + GuestOSVO guestOS = mock(GuestOSVO.class); + when(guestOS.getId()).thenReturn(40L); + when(guestOSDao.findOneByDisplayName(osDisplayName)).thenReturn(guestOS); + when(guestOSDao.listLikeDisplayName(osDisplayName)).thenReturn(List.of(guestOS)); + + Long resolvedGuestOsId = unmanagedVMsManager.resolveVmwareToKvmImportGuestOsId(null, instance); + + Assert.assertEquals(Long.valueOf(40L), resolvedGuestOsId); + } + + @Test + public void testResolveVmwareToKvmImportGuestOsIdAvoidsWeakGuestOsTypeMatch() { + String osDisplayName = "Linux"; + instance.setOperatingSystem(osDisplayName); + instance.setOperatingSystemId(null); + instance.setHostHypervisorVersion("8.0.3"); + + GuestOSVO guestOS = mock(GuestOSVO.class); + when(guestOS.getDisplayName()).thenReturn("Other Linux (64-bit)"); + when(guestOSDao.findOneByDisplayName(osDisplayName)).thenReturn(null); + when(guestOSDao.listLikeDisplayName(osDisplayName)).thenReturn(List.of(guestOS)); + + Long resolvedGuestOsId = unmanagedVMsManager.resolveVmwareToKvmImportGuestOsId(null, instance); + + Assert.assertNull(resolvedGuestOsId); + } + private enum VcenterParameter { EXISTING, EXTERNAL, diff --git a/ui/src/views/tools/ManageInstances.vue b/ui/src/views/tools/ManageInstances.vue index 6f625961ea8f..e611dec5491e 100644 --- a/ui/src/views/tools/ManageInstances.vue +++ b/ui/src/views/tools/ManageInstances.vue @@ -1379,11 +1379,7 @@ export default { this.fetchInstances() } }, - async fetchGuestOsMappings (osIdentifier, hypervisorVersion) { - const params = {} - params.hypervisor = 'VMware' - params.hypervisorversion = hypervisorVersion - params.osnameforhypervisor = osIdentifier + async fetchGuestOsMappingsByParams (params) { return await getAPI('listGuestOsMapping', params).then(json => { return json.listguestosmappingresponse?.guestosmapping || [] }).catch(error => { @@ -1391,6 +1387,84 @@ export default { return [] }) }, + normalizeGuestOsName (name) { + return (name || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + }, + isStrongGuestOsNameMatch (osType, osDisplayName) { + const candidate = this.normalizeGuestOsName(osType.description || osType.osdisplayname) + const source = this.normalizeGuestOsName(osDisplayName) + if (!candidate || !source) { + return false + } + if (candidate === source) { + return true + } + if (Math.min(candidate.length, source.length) < 8) { + return false + } + return candidate.includes(source) || source.includes(candidate) + }, + async fetchGuestOsTypeFallbackMappings (osDisplayName) { + if (!osDisplayName || !('listOsTypes' in this.$store.getters.apis)) { + return [] + } + return await getAPI('listOsTypes', { + description: osDisplayName + }).then(json => { + const osTypes = json.listostypesresponse?.ostype || [] + return osTypes + .filter(osType => this.isStrongGuestOsNameMatch(osType, osDisplayName)) + .map(osType => { + return { + ostypeid: osType.id, + osdisplayname: osType.description || osType.osdisplayname + } + }) + }).catch(error => { + this.$notifyError(error) + return [] + }) + }, + async fetchGuestOsMappings (osIdentifier, osDisplayName, hypervisorVersion) { + const lookups = [] + if (osDisplayName) { + lookups.push({ + hypervisor: 'VMware', + hypervisorversion: hypervisorVersion, + osdisplayname: osDisplayName + }) + } + if (osIdentifier) { + lookups.push({ + hypervisor: 'VMware', + hypervisorversion: hypervisorVersion, + osnameforhypervisor: osIdentifier + }) + } + if (osDisplayName) { + lookups.push({ + hypervisor: 'VMware', + osdisplayname: osDisplayName + }) + } + if (osIdentifier) { + lookups.push({ + hypervisor: 'VMware', + osnameforhypervisor: osIdentifier + }) + } + + for (const params of lookups) { + if (!params.hypervisorversion) { + delete params.hypervisorversion + } + const mappings = await this.fetchGuestOsMappingsByParams(params) + if (mappings.length > 0) { + return mappings + } + } + return await this.fetchGuestOsTypeFallbackMappings(osDisplayName) + }, fetchVmwareInstanceForKVMMigration (vmname, hostname) { const params = {} this.loadingGuestOsMappings = true @@ -1411,7 +1485,10 @@ export default { this.selectedUnmanagedInstance = response.unmanagedinstance[0] this.selectedUnmanagedInstance.ostypename = this.selectedUnmanagedInstance.osdisplayname this.selectedUnmanagedInstance.state = this.selectedUnmanagedInstance.powerstate - this.selectedUnmanagedInstance.guestOsMappings = await this.fetchGuestOsMappings(this.selectedUnmanagedInstance.osid, this.selectedUnmanagedInstance.hypervisorversion) + this.selectedUnmanagedInstance.guestOsMappings = await this.fetchGuestOsMappings( + this.selectedUnmanagedInstance.osid, + this.selectedUnmanagedInstance.osdisplayname, + this.selectedUnmanagedInstance.hypervisorversion) }).catch(error => { this.$notifyError(error) }).finally(() => { From bbe7804230958831cede162252eef968c927e2a7 Mon Sep 17 00:00:00 2001 From: andrijapanicsb <45762285+andrijapanicsb@users.noreply.github.com> Date: Thu, 28 May 2026 05:36:23 +0200 Subject: [PATCH 2/2] Fix VMware import guest OS mapping fallback Signed-off-by: andrijapanicsb <45762285+andrijapanicsb@users.noreply.github.com> --- .../cloud/server/ManagementServerImpl.java | 7 +-- .../views/tools/ImportUnmanagedInstance.vue | 2 +- ui/src/views/tools/ManageInstances.vue | 45 +++++++++++++++---- 3 files changed, 42 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/cloud/server/ManagementServerImpl.java b/server/src/main/java/com/cloud/server/ManagementServerImpl.java index e5066b6da5cc..a07e071e5553 100644 --- a/server/src/main/java/com/cloud/server/ManagementServerImpl.java +++ b/server/src/main/java/com/cloud/server/ManagementServerImpl.java @@ -3077,10 +3077,11 @@ public Pair, Integer> listGuestOSMappingByCrit if (osDisplayName != null) { List guestOSVOS = _guestOSDao.listLikeDisplayName(osDisplayName); - if (CollectionUtils.isNotEmpty(guestOSVOS)) { - List guestOSids = guestOSVOS.stream().map(mo -> mo.getId()).collect(Collectors.toList()); - sc.addAnd(guestOsId, SearchCriteria.Op.IN, guestOSids.toArray()); + if (CollectionUtils.isEmpty(guestOSVOS)) { + return new Pair<>(Collections.emptyList(), 0); } + List guestOSids = guestOSVOS.stream().map(mo -> mo.getId()).collect(Collectors.toList()); + sc.addAnd(guestOsId, SearchCriteria.Op.IN, guestOSids.toArray()); } final Pair, Integer> result = _guestOSHypervisorDao.searchAndCount(sc, searchFilter); diff --git a/ui/src/views/tools/ImportUnmanagedInstance.vue b/ui/src/views/tools/ImportUnmanagedInstance.vue index ffa0d9344335..de31e353cd4e 100644 --- a/ui/src/views/tools/ImportUnmanagedInstance.vue +++ b/ui/src/views/tools/ImportUnmanagedInstance.vue @@ -253,7 +253,7 @@ :filterOption="(input, option) => { return option.label.toLowerCase().indexOf(input.toLowerCase()) >= 0 }"> - + {{ mapping.osdisplayname }} diff --git a/ui/src/views/tools/ManageInstances.vue b/ui/src/views/tools/ManageInstances.vue index e611dec5491e..ad018208d28e 100644 --- a/ui/src/views/tools/ManageInstances.vue +++ b/ui/src/views/tools/ManageInstances.vue @@ -1388,7 +1388,12 @@ export default { }) }, normalizeGuestOsName (name) { - return (name || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim() + return (name || '') + .toLowerCase() + .replace(/^microsoft\s+/, '') + .replace(/[^a-z0-9]+/g, ' ') + .replace(/\s+/g, ' ') + .trim() }, isStrongGuestOsNameMatch (osType, osDisplayName) { const candidate = this.normalizeGuestOsName(osType.description || osType.osdisplayname) @@ -1425,32 +1430,55 @@ export default { return [] }) }, + filterGuestOsMappings (mappings, params, osDisplayName) { + if (!mappings || mappings.length === 0) { + return [] + } + + const osNameForHypervisor = (params.osnameforhypervisor || '').toLowerCase() + let filteredMappings = mappings + if (osNameForHypervisor) { + filteredMappings = mappings.filter(mapping => (mapping.osnameforhypervisor || '').toLowerCase() === osNameForHypervisor) + } + + if (osDisplayName) { + const displayNameMatches = filteredMappings.filter(mapping => this.isStrongGuestOsNameMatch(mapping, osDisplayName)) + if (displayNameMatches.length > 0) { + return displayNameMatches + } + if (params.osdisplayname) { + return [] + } + } + + return filteredMappings + }, async fetchGuestOsMappings (osIdentifier, osDisplayName, hypervisorVersion) { const lookups = [] - if (osDisplayName) { + if (osIdentifier) { lookups.push({ hypervisor: 'VMware', hypervisorversion: hypervisorVersion, - osdisplayname: osDisplayName + osnameforhypervisor: osIdentifier }) } if (osIdentifier) { lookups.push({ hypervisor: 'VMware', - hypervisorversion: hypervisorVersion, osnameforhypervisor: osIdentifier }) } if (osDisplayName) { lookups.push({ hypervisor: 'VMware', + hypervisorversion: hypervisorVersion, osdisplayname: osDisplayName }) } - if (osIdentifier) { + if (osDisplayName) { lookups.push({ hypervisor: 'VMware', - osnameforhypervisor: osIdentifier + osdisplayname: osDisplayName }) } @@ -1459,8 +1487,9 @@ export default { delete params.hypervisorversion } const mappings = await this.fetchGuestOsMappingsByParams(params) - if (mappings.length > 0) { - return mappings + const filteredMappings = this.filterGuestOsMappings(mappings, params, osDisplayName) + if (filteredMappings.length > 0) { + return filteredMappings } } return await this.fetchGuestOsTypeFallbackMappings(osDisplayName)