Fix and de-risk UserVmManagerImpl external-DHCP startup scan

This commit is contained in:
Pearl Dsilva 2026-07-16 13:34:58 -04:00
parent e8df87e89b
commit 1f081f37db
6 changed files with 129 additions and 18 deletions

View File

@ -190,4 +190,6 @@ public interface VMInstanceDao extends GenericDao<VMInstanceVO, Long>, StateDao<
int getVmCountByOfferingId(Long serviceOfferingId);
int getVmCountByOfferingNotInDomain(Long serviceOfferingId, List<Long> domainIds);
List<VMInstanceVO> listByIds(List<Long> ids);
}

View File

@ -1261,4 +1261,17 @@ public class VMInstanceDaoImpl extends GenericDaoBase<VMInstanceVO, Long> implem
List<Integer> count = customSearch(sc, null);
return count.get(0);
}
@Override
public List<VMInstanceVO> listByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return new ArrayList<>();
}
SearchBuilder<VMInstanceVO> sb = createSearchBuilder();
sb.and("id", sb.entity().getId(), Op.IN);
sb.done();
SearchCriteria<VMInstanceVO> sc = sb.create();
sc.setParameters("id", ids.toArray());
return listBy(sc);
}
}

View File

@ -965,7 +965,11 @@ public class NetworkModelImpl extends ManagerBase implements NetworkModel, Confi
Network network = _networksDao.findById(networkId);
if (network != null && network.getGuestType() != GuestType.Shared) {
if (network == null) {
return false;
}
if (network.getGuestType() != GuestType.Shared) {
return false;
}

View File

@ -54,6 +54,7 @@ import javax.naming.ConfigurationException;
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 class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir
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<NetworkVO> networks = _networkDao.listByGuestType(Network.GuestType.Shared);
networks.addAll(_networkDao.listByGuestType(Network.GuestType.L2));
List<NetworkVO> networks = _networkDao.listByGuestType(Network.GuestType.Shared);
Map<Long, Boolean> offeringWithoutServices = new HashMap<>();
int networksScanned = 0;
int nicsAdded = 0;
for (NetworkVO network: networks) {
if (GuestType.L2.equals(network.getGuestType()) || _networkModel.isSharedNetworkWithoutServices(network.getId())) {
List<NicVO> nics = _nicDao.listByNetworkId(network.getId());
for (NetworkVO network : networks) {
boolean withoutServices = offeringWithoutServices.computeIfAbsent(network.getNetworkOfferingId(),
offeringId -> _networkModel.listNetworkOfferingServices(offeringId).isEmpty());
if (!withoutServices) {
continue;
}
networksScanned++;
for (NicVO nic : nics) {
if (nic.getIPv4Address() == null) {
long nicId = nic.getId();
long vmId = nic.getInstanceId();
VMInstanceVO vmInstance = _vmInstanceDao.findById(vmId);
List<NicVO> nullIpNics = _nicDao.listByNetworkId(network.getId()).stream()
.filter(nic -> nic.getIPv4Address() == null)
.collect(Collectors.toList());
if (nullIpNics.isEmpty()) {
continue;
}
// 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);
}
List<Long> vmIds = nullIpNics.stream().map(NicVO::getInstanceId).distinct().collect(Collectors.toList());
Map<Long, VMInstanceVO> runningVmsById = _vmInstanceDao.listByIds(vmIds).stream()
.filter(vm -> vm != null && vm.getState() == State.Running)
.collect(Collectors.toMap(VMInstanceVO::getId, vm -> vm));
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);
}
}

View File

@ -21,6 +21,7 @@ import com.cloud.dc.VlanVO;
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 class NetworkModelImplTest {
Map<Network.Provider, ArrayList<PublicIpAddress>> 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));
}
}

View File

@ -33,9 +33,11 @@ import static org.mockito.Mockito.doReturn;
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.LinkedList;
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 class UserVmManagerImplTest {
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<Long, ?> vmIdCountMap = (Map<Long, ?>) 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();
}
}