From 1f081f37db4dde85fd647145fdccf622becc3e48 Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 16 Jul 2026 13:34:58 -0400 Subject: [PATCH] Fix and de-risk UserVmManagerImpl external-DHCP startup scan --- .../java/com/cloud/vm/dao/VMInstanceDao.java | 2 + .../com/cloud/vm/dao/VMInstanceDaoImpl.java | 13 ++++ .../com/cloud/network/NetworkModelImpl.java | 6 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 57 +++++++++++------ .../cloud/network/NetworkModelImplTest.java | 10 +++ .../com/cloud/vm/UserVmManagerImplTest.java | 61 +++++++++++++++++++ 6 files changed, 130 insertions(+), 19 deletions(-) diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java index 56e16ddd871c..ff7e352a4822 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDao.java @@ -190,4 +190,6 @@ List searchRemovedByRemoveDate(final Date startDate, final Date en int getVmCountByOfferingId(Long serviceOfferingId); int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List domainIds); + + List listByIds(List ids); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java index 518bc3cf497c..9c129a24229c 100755 --- a/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/VMInstanceDaoImpl.java @@ -1261,4 +1261,17 @@ public int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List do List count = customSearch(sc, null); return count.get(0); } + + @Override + public List listByIds(List ids) { + if (ids == null || ids.isEmpty()) { + return new ArrayList<>(); + } + SearchBuilder sb = createSearchBuilder(); + sb.and("id", sb.entity().getId(), Op.IN); + sb.done(); + SearchCriteria sc = sb.create(); + sc.setParameters("id", ids.toArray()); + return listBy(sc); + } } diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index 86791b87851f..ad404528b758 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -965,7 +965,11 @@ public boolean isSharedNetworkWithoutServices (long networkId) { Network network = _networksDao.findById(networkId); - if (network != null && network.getGuestType() != GuestType.Shared) { + if (network == null) { + return false; + } + + if (network.getGuestType() != GuestType.Shared) { return false; } diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index 7e58cd01050a..3bff22c8c0e8 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -54,6 +54,7 @@ import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.ParserConfigurationException; +import com.cloud.utils.Profiler; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; @@ -2452,33 +2453,53 @@ public String getName() { public boolean start() { _executor.scheduleWithFixedDelay(new ExpungeTask(), _expungeInterval, _expungeInterval, TimeUnit.SECONDS); _vmIpFetchExecutor.scheduleWithFixedDelay(new VmIpFetchTask(), VmIpFetchWaitInterval.value(), VmIpFetchWaitInterval.value(), TimeUnit.SECONDS); - loadVmDetailsInMapForExternalDhcpIp(); + _vmIpFetchExecutor.submit(this::loadVmDetailsInMapForExternalDhcpIp); return true; } - private void loadVmDetailsInMapForExternalDhcpIp() { + protected void loadVmDetailsInMapForExternalDhcpIp() { + try { + Profiler profiler = new Profiler(); + profiler.start(); + + List networks = _networkDao.listByGuestType(Network.GuestType.Shared); + Map offeringWithoutServices = new HashMap<>(); + int networksScanned = 0; + int nicsAdded = 0; + + for (NetworkVO network : networks) { + boolean withoutServices = offeringWithoutServices.computeIfAbsent(network.getNetworkOfferingId(), + offeringId -> _networkModel.listNetworkOfferingServices(offeringId).isEmpty()); + if (!withoutServices) { + continue; + } + networksScanned++; - List networks = _networkDao.listByGuestType(Network.GuestType.Shared); - networks.addAll(_networkDao.listByGuestType(Network.GuestType.L2)); + List nullIpNics = _nicDao.listByNetworkId(network.getId()).stream() + .filter(nic -> nic.getIPv4Address() == null) + .collect(Collectors.toList()); + if (nullIpNics.isEmpty()) { + continue; + } - for (NetworkVO network: networks) { - if (GuestType.L2.equals(network.getGuestType()) || _networkModel.isSharedNetworkWithoutServices(network.getId())) { - List nics = _nicDao.listByNetworkId(network.getId()); + List vmIds = nullIpNics.stream().map(NicVO::getInstanceId).distinct().collect(Collectors.toList()); + Map runningVmsById = _vmInstanceDao.listByIds(vmIds).stream() + .filter(vm -> vm != null && vm.getState() == State.Running) + .collect(Collectors.toMap(VMInstanceVO::getId, vm -> vm)); - for (NicVO nic : nics) { - if (nic.getIPv4Address() == null) { - long nicId = nic.getId(); - long vmId = nic.getInstanceId(); - VMInstanceVO vmInstance = _vmInstanceDao.findById(vmId); - - // only load running vms. For stopped vms get loaded on starting - if (vmInstance != null && vmInstance.getState() == State.Running) { - VmAndCountDetails vmAndCount = new VmAndCountDetails(vmId, VmIpFetchTrialMax.value()); - vmIdCountMap.put(nicId, vmAndCount); - } + for (NicVO nic : nullIpNics) { + if (runningVmsById.containsKey(nic.getInstanceId())) { + vmIdCountMap.put(nic.getId(), new VmAndCountDetails(nic.getInstanceId(), VmIpFetchTrialMax.value())); + nicsAdded++; } } } + + profiler.stop(); + logger.info("External-DHCP VM-IP map seeded: {} shared-without-service networks, {} nics added, took {} ms", + networksScanned, nicsAdded, profiler.getDurationInMillis()); + } catch (Exception e) { + logger.error("Failed to seed external-DHCP VM-IP retrieval map", e); } } diff --git a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java index 889031f2b50b..d0054abf5b8b 100644 --- a/server/src/test/java/com/cloud/network/NetworkModelImplTest.java +++ b/server/src/test/java/com/cloud/network/NetworkModelImplTest.java @@ -21,6 +21,7 @@ import com.cloud.exception.InvalidParameterValueException; import com.cloud.network.addr.PublicIp; import com.cloud.network.dao.IPAddressVO; +import com.cloud.network.dao.NetworkDao; import com.cloud.network.dao.NetworkServiceMapDao; import com.cloud.network.dao.NetworkServiceMapVO; import com.cloud.network.dao.NetworkVO; @@ -232,4 +233,13 @@ public void testGetProviderToIpList() { Map> result = networkModel.getProviderToIpList(network, ipToServices); Assert.assertNotNull(result); } + + @Test + public void testIsSharedNetworkWithoutServicesReturnsFalseWhenNetworkMissing() { + NetworkDao networksDao = Mockito.mock(NetworkDao.class); + ReflectionTestUtils.setField(networkModel, "_networksDao", networksDao); + Mockito.when(networksDao.findById(123L)).thenReturn(null); + + Assert.assertFalse(networkModel.isSharedNetworkWithoutServices(123L)); + } } diff --git a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java index b21cd53d743c..566da7432145 100644 --- a/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java +++ b/server/src/test/java/com/cloud/vm/UserVmManagerImplTest.java @@ -33,9 +33,11 @@ import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.when; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -45,6 +47,7 @@ import java.util.List; import java.util.Map; +import com.cloud.vm.dao.VMInstanceDao; import org.apache.cloudstack.acl.ControlledEntity; import org.apache.cloudstack.acl.SecurityChecker; import org.apache.cloudstack.api.ApiConstants; @@ -188,6 +191,9 @@ public class UserVmManagerImplTest { @Mock protected NicDao nicDao; + @Mock + protected VMInstanceDao vmInstanceDao; + @Mock private NetworkDao _networkDao; @@ -3207,4 +3213,59 @@ public void verifyVmLimits_constrainedOffering_throwsException() { userVmManagerImpl.verifyVmLimits(userVmVoMock, customParameters)); Assert.assertTrue(ex.getMessage().startsWith("The CPU speed of this offering")); } + + @Test + public void testLoadVmDetailsInMapForExternalDhcpIpSeedsOnlyRunningNullIpNics() { + NetworkVO network = mock(NetworkVO.class); + when(network.getId()).thenReturn(100L); + when(network.getNetworkOfferingId()).thenReturn(7L); + when(_networkDao.listByGuestType(Network.GuestType.Shared)).thenReturn(Arrays.asList(network)); + when(networkModel.listNetworkOfferingServices(7L)).thenReturn(new ArrayList<>()); + + NicVO runningNullIp = mock(NicVO.class); + when(runningNullIp.getId()).thenReturn(11L); + when(runningNullIp.getInstanceId()).thenReturn(1000L); + when(runningNullIp.getIPv4Address()).thenReturn(null); + + NicVO withIp = mock(NicVO.class); + when(withIp.getIPv4Address()).thenReturn("10.1.1.5"); + + NicVO stoppedNullIp = mock(NicVO.class); + when(stoppedNullIp.getInstanceId()).thenReturn(2000L); + when(stoppedNullIp.getIPv4Address()).thenReturn(null); + + when(nicDao.listByNetworkId(100L)).thenReturn(Arrays.asList(runningNullIp, withIp, stoppedNullIp)); + + VMInstanceVO running = mock(VMInstanceVO.class); + when(running.getId()).thenReturn(1000L); + when(running.getState()).thenReturn(VirtualMachine.State.Running); + VMInstanceVO stopped = mock(VMInstanceVO.class); + when(stopped.getState()).thenReturn(VirtualMachine.State.Stopped); + when(vmInstanceDao.listByIds(Mockito.anyList())).thenReturn(Arrays.asList(running, stopped)); + + userVmManagerImpl.loadVmDetailsInMapForExternalDhcpIp(); + + @SuppressWarnings("unchecked") + Map vmIdCountMap = (Map) ReflectionTestUtils.getField(userVmManagerImpl, "vmIdCountMap"); + assertNotNull(vmIdCountMap); + assertEquals(1, vmIdCountMap.size()); + assertTrue(vmIdCountMap.containsKey(11L)); + assertFalse(vmIdCountMap.containsKey(12L)); + Mockito.verify(vmInstanceDao, times(1)).listByIds(Mockito.anyList()); + Mockito.verify(vmInstanceDao, Mockito.never()).findById(Mockito.anyLong()); + } + + @Test + public void testStartSeedsExternalDhcpMapAsynchronously() { + java.util.concurrent.ScheduledExecutorService expungeExecutor = mock(java.util.concurrent.ScheduledExecutorService.class); + java.util.concurrent.ScheduledExecutorService ipFetchExecutor = mock(java.util.concurrent.ScheduledExecutorService.class); + ReflectionTestUtils.setField(userVmManagerImpl, "_executor", expungeExecutor); + ReflectionTestUtils.setField(userVmManagerImpl, "_vmIpFetchExecutor", ipFetchExecutor); + + boolean result = userVmManagerImpl.start(); + + assertTrue(result); + Mockito.verify(ipFetchExecutor, times(1)).submit(any(Runnable.class)); + Mockito.verify(userVmManagerImpl, Mockito.never()).loadVmDetailsInMapForExternalDhcpIp(); + } }