diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4edd448067ae..c84df16470be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -149,7 +149,8 @@ jobs: smoke/test_vpc_ipv6 smoke/test_vpc_redundant smoke/test_vpc_router_nics - smoke/test_vpc_vpn", + smoke/test_vpc_vpn + smoke/test_vpc_hypervisor_max_networks", "component/find_hosts_for_migration component/test_acl_isolatednetwork component/test_acl_isolatednetwork_delete diff --git a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java index 31b08429cc44..12d11fb140e5 100644 --- a/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java +++ b/engine/api/src/main/java/org/apache/cloudstack/engine/orchestration/service/NetworkOrchestrationService.java @@ -108,6 +108,14 @@ public interface NetworkOrchestrationService { ConfigKey TUNGSTEN_ENABLED = new ConfigKey<>(Boolean.class, "tungsten.plugin.enable", "Advanced", "false", "Indicates whether to enable the Tungsten plugin", false, ConfigKey.Scope.Zone, null); + ConfigKey VirtualMachineMaxNicsKvm = new ConfigKey<>("Advanced", Integer.class, "virtual.machine.max.nics.kvm", "23", + "The maximum number of NICs supported by the KVM hypervsior.", true, Scope.Cluster); + + ConfigKey VirtualMachineMaxNicsVmware = new ConfigKey<>("Advanced", Integer.class, "virtual.machine.max.nics.vmware", "10", + "The maximum number of NICs supported by the VMware hypervsior.", true, Scope.Cluster); + + ConfigKey VirtualMachineMaxNicsXenserver = new ConfigKey<>("Advanced", Integer.class, "virtual.machine.max.nics.xenserver", "7", + "The maximum number of NICs supported by the XenServer hypervsior.", true, Scope.Cluster); ConfigKey NSX_ENABLED = new ConfigKey<>(Boolean.class, "nsx.plugin.enable", "Advanced", "false", "Indicates whether to enable the NSX plugin", false, ConfigKey.Scope.Zone, null); @@ -363,5 +371,16 @@ void implementNetworkElementsAndResources(DeployDestination dest, ReservationCon void unmanageNics(VirtualMachineProfile vm); + /** + * Returns the maximum number of NICs that the given virtual machine can have considering its hypervisor. + *

+ * First we try to retrieve the setting value from the cluster where the virtual machine is deployed. If the cluster does not exist, we try to retrieve the setting value from the virtual machine hypervisor type. + * Returns null if the setting value could not be extracted. + * + * @param virtualMachine Virtual machine to get the maximum number of NICs. + * @return The maximum number of NICs that the virtual machine can have. + */ + Integer getVirtualMachineMaxNicsValue(VirtualMachine virtualMachine); + void expungeLbVmRefs(List vmIds, Long batchSize); } diff --git a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java index e7f41d079a74..5b89a5588e18 100644 --- a/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java +++ b/engine/components-api/src/main/java/com/cloud/network/vpc/VpcManager.java @@ -23,6 +23,7 @@ import com.cloud.network.dao.IPAddressVO; import com.cloud.utils.Pair; import org.apache.cloudstack.acl.ControlledEntity.ACLType; +import org.apache.cloudstack.framework.config.ConfigKey; import com.cloud.exception.ConcurrentOperationException; import com.cloud.exception.InsufficientAddressCapacityException; @@ -38,9 +39,10 @@ import com.cloud.network.addr.PublicIp; import com.cloud.offering.NetworkOffering; import com.cloud.user.Account; -import org.apache.cloudstack.framework.config.ConfigKey; public interface VpcManager { + ConfigKey VpcMaxNetworks = new ConfigKey<>("Advanced", Integer.class, "vpc.max.networks", "3", + "Maximum number of networks per VPC.", true, ConfigKey.Scope.Account); ConfigKey VpcTierNamePrepend = new ConfigKey<>(Boolean.class, "vpc.tier.name.prepend", ConfigKey.CATEGORY_NETWORK, diff --git a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java index ae07c4c7bc46..f6c76e91d2ca 100644 --- a/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java +++ b/engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java @@ -1141,6 +1141,13 @@ private NicVO checkForRaceAndAllocateNic(final NicProfile requested, final Netwo public Pair allocateNic(final NicProfile requested, final Network network, final Boolean isDefaultNic, int deviceId, final VirtualMachineProfile vm) throws InsufficientVirtualNetworkCapacityException, InsufficientAddressCapacityException, ConcurrentOperationException { + Integer virtualMachineMaxNicsValue = getVirtualMachineMaxNicsValue(vm.getVirtualMachine()); + List nics = _nicDao.listByVmId(vm.getId()); + + if (virtualMachineMaxNicsValue != null && nics.size() >= virtualMachineMaxNicsValue) { + throw new CloudRuntimeException(String.format("Failed to allocate NIC on network [%s] because VM [%s] has reached its maximum NIC capacity [%s].", network.getUuid(), vm.getUuid(), virtualMachineMaxNicsValue)); + } + if (requested != null && requested.getMode() == null) { requested.setMode(network.getMode()); } @@ -1159,6 +1166,91 @@ public Pair allocateNic(final NicProfile requested, final N return new Pair<>(vmNic, Integer.valueOf(deviceId)); } + @Override + public Integer getVirtualMachineMaxNicsValue(VirtualMachine virtualMachine) { + Integer virtualMachineMaxNicsValue = getVirtualMachineMaxNicsValueFromCluster(virtualMachine); + + if (virtualMachineMaxNicsValue != null) { + return virtualMachineMaxNicsValue; + } + + if (virtualMachine.getHypervisorType() == null) { + logger.debug("Not considering the hypervisor maximum limits as we were unable to find a compatible hypervisor on the VM and VM cluster for virtual machine maximum NICs settings."); + return null; + } + + return getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachine); + } + + /** + * Searches the maximum virtual machine NICs based on the cluster where the instance is deployed. + * + * @param virtualMachine Virtual machine to get the cluster. + * @return The maximum number of NICs that the virtual machine can have. + */ + protected Integer getVirtualMachineMaxNicsValueFromCluster(VirtualMachine virtualMachine) { + HostVO host = _hostDao.findById(virtualMachine.getHostId()); + if (host == null) { + return null; + } + + ClusterVO cluster = clusterDao.findById(host.getClusterId()); + if (cluster == null) { + return null; + } + + return getVirtualMachineMaxNicsValueFromCluster(cluster); + } + + /** + * Searches the maximum virtual machine NICs based on the hypervisor type of the cluster where the instance is deployed. + * + * @param cluster Cluster to get the virtual machine max NICs value from. + * @return The maximum number of NICs that the virtual machine can have. + */ + protected Integer getVirtualMachineMaxNicsValueFromCluster(ClusterVO cluster) { + HypervisorType clusterHypervisor = cluster.getHypervisorType(); + String logMessage = "The cluster {} where the VM is deployed uses the {} hypervisor. Therefore, the {} setting value [{}] will be used."; + + if (clusterHypervisor.equals(HypervisorType.KVM)) { + logger.debug(logMessage, cluster.getName(), clusterHypervisor, VirtualMachineMaxNicsKvm, VirtualMachineMaxNicsKvm.valueIn(cluster.getId())); + return VirtualMachineMaxNicsKvm.valueIn(cluster.getId()); + } else if (clusterHypervisor.equals(HypervisorType.VMware)) { + logger.debug(logMessage, cluster.getName(), clusterHypervisor, VirtualMachineMaxNicsKvm, VirtualMachineMaxNicsVmware.valueIn(cluster.getId())); + return VirtualMachineMaxNicsVmware.valueIn(cluster.getId()); + } else if (clusterHypervisor.equals(HypervisorType.XenServer)) { + logger.debug(logMessage, cluster.getName(), clusterHypervisor, VirtualMachineMaxNicsXenserver, VirtualMachineMaxNicsXenserver.valueIn(cluster.getId())); + return VirtualMachineMaxNicsXenserver.valueIn(cluster.getId()); + } + + return null; + } + + /** + * Searches the maximum virtual machine NICs based on the virtual machine hypervisor type. + * + * @param virtualMachine Virtual machine to get the hypervisor type. + * @return The maximum number of NICs that the virtual machine can have. + */ + protected Integer getVirtualMachineMaxNicsValueFromVmHypervisorType(VirtualMachine virtualMachine) { + HypervisorType virtualMachineHypervisorType = virtualMachine.getHypervisorType(); + String logMessage = "Using the {} setting global value {} as the VM {} has the {} hypervisor type and is not deployed on either a host or a cluster."; + + if (virtualMachineHypervisorType.equals(HypervisorType.KVM)) { + logger.debug(logMessage, VirtualMachineMaxNicsKvm, VirtualMachineMaxNicsKvm.value(), virtualMachine.getUuid(), virtualMachineHypervisorType); + return VirtualMachineMaxNicsKvm.value(); + } else if (virtualMachineHypervisorType.equals(HypervisorType.VMware)) { + logger.debug(logMessage, VirtualMachineMaxNicsVmware, VirtualMachineMaxNicsVmware.value(), virtualMachine.getUuid(), virtualMachineHypervisorType); + return VirtualMachineMaxNicsVmware.value(); + } else if (virtualMachineHypervisorType.equals(HypervisorType.XenServer)) { + logger.debug(logMessage, VirtualMachineMaxNicsXenserver, VirtualMachineMaxNicsXenserver.value(), virtualMachine.getUuid(), virtualMachineHypervisorType); + return VirtualMachineMaxNicsXenserver.value(); + } + + logger.debug("Not considering the hypervisor maximum limits as we were unable to find a compatible hypervisor on the VM and VM cluster for virtual machine maximum NICs configurations."); + return null; + } + private boolean isNicAllocatedForProviderPublicNetworkOnVR(Network network, NicProfile requested, VirtualMachineProfile vm, Provider provider) { if (ObjectUtils.anyNull(network, requested, vm)) { return false; @@ -4918,6 +5010,6 @@ public ConfigKey[] getConfigKeys() { return new ConfigKey[]{NetworkGcWait, NetworkGcInterval, NetworkLockTimeout, DeniedRoutes, GuestDomainSuffix, NetworkThrottlingRate, MinVRVersion, PromiscuousMode, MacAddressChanges, ForgedTransmits, MacLearning, RollingRestartEnabled, - TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN}; + TUNGSTEN_ENABLED, NSX_ENABLED, NETRIS_ENABLED, NETWORK_LB_HAPROXY_MAX_CONN, VirtualMachineMaxNicsKvm, VirtualMachineMaxNicsVmware, VirtualMachineMaxNicsXenserver}; } } diff --git a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java index a95f0e364759..bb89a56a9790 100644 --- a/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java +++ b/engine/orchestration/src/test/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestratorTest.java @@ -24,6 +24,8 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.net.URI; import java.util.ArrayList; import java.util.Arrays; @@ -31,10 +33,15 @@ import java.util.List; import java.util.Map; +import com.cloud.dc.ClusterVO; import com.cloud.dc.DataCenter; +import com.cloud.dc.dao.ClusterDao; import com.cloud.exception.InsufficientVirtualNetworkCapacityException; +import com.cloud.host.HostVO; +import com.cloud.host.dao.HostDao; import com.cloud.network.IpAddressManager; import com.cloud.utils.Pair; +import org.apache.cloudstack.framework.config.ConfigKey; import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -135,6 +142,9 @@ public void setUp() { testOrchestrator.routerJoinDao = mock(DomainRouterJoinDao.class); testOrchestrator._ipAddrMgr = mock(IpAddressManager.class); testOrchestrator._entityMgr = mock(EntityManager.class); + testOrchestrator._hostDao = mock(HostDao.class); + testOrchestrator.clusterDao = mock(ClusterDao.class); + DhcpServiceProvider provider = mock(DhcpServiceProvider.class); Map capabilities = new HashMap(); @@ -1009,4 +1019,180 @@ public void testImportNicWithIP4Address() throws Exception { assertEquals("testtag", nicProfile.getName()); } } + + @Test + public void getVirtualMachineMaxNicsValueTestVirtualMachineDeployedReturnsVirtualMachineClusterMaxNics() { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(44).when(testOrchestrator).getVirtualMachineMaxNicsValueFromCluster(virtualMachineMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValue(virtualMachineMock); + + Mockito.verify(testOrchestrator, Mockito.times(1)).getVirtualMachineMaxNicsValueFromCluster((VirtualMachine) Mockito.any()); + Assert.assertEquals((Integer) 44, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueTestVirtualMachineIncompatibleHypervisorReturnsNull() { + VirtualMachineProfile virtualMachineProfileMock = Mockito.mock(VirtualMachineProfile.class); + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(virtualMachineMock).when(virtualMachineProfileMock).getVirtualMachine(); + Mockito.doReturn(null).when(virtualMachineMock).getHypervisorType(); + Mockito.doReturn(null).when(testOrchestrator).getVirtualMachineMaxNicsValueFromCluster(virtualMachineMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValue(virtualMachineMock); + + Assert.assertNull(virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueTestVirtualMachineWithoutDeployReturnsVirtualMachineHypervisorTypeMaxNics() { + VirtualMachineProfile virtualMachineProfileMock = Mockito.mock(VirtualMachineProfile.class); + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(virtualMachineMock).when(virtualMachineProfileMock).getVirtualMachine(); + Mockito.doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + Mockito.doReturn(null).when(testOrchestrator).getVirtualMachineMaxNicsValueFromCluster(virtualMachineMock); + Mockito.doReturn(33).when(testOrchestrator).getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachineMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValue(virtualMachineMock); + + Mockito.verify(testOrchestrator, Mockito.times(1)).getVirtualMachineMaxNicsValueFromVmHypervisorType(Mockito.any()); + Assert.assertEquals((Integer) 33, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestHostDoesNotExistReturnsNull() { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(100L).when(virtualMachineMock).getHostId(); + Mockito.doReturn(null).when(testOrchestrator._hostDao).findById(100L); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(virtualMachineMock); + + Assert.assertNull(virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestClusterDoesNotExistReturnsNull() { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + HostVO hostVoMock = Mockito.mock(HostVO.class); + Mockito.doReturn(1L).when(virtualMachineMock).getHostId(); + Mockito.doReturn(hostVoMock).when(testOrchestrator._hostDao).findById(1L); + Mockito.doReturn(100L).when(hostVoMock).getClusterId(); + Mockito.doReturn(null).when(testOrchestrator.clusterDao).findById(100L); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(virtualMachineMock); + + Assert.assertNull(virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestKvmClusterReturnsVirtualMachineMaxNicsKvmClusterValue() throws NoSuchFieldException, IllegalAccessException { + ClusterVO clusterVoMock = Mockito.mock(ClusterVO.class); + ConfigKey virtualMachineMaxNicsKvmMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(1L).when(clusterVoMock).getId(); + Mockito.doReturn(Hypervisor.HypervisorType.KVM).when(clusterVoMock).getHypervisorType(); + Mockito.doReturn(33).when(virtualMachineMaxNicsKvmMock).valueIn(1L); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsKvm"), virtualMachineMaxNicsKvmMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(clusterVoMock); + + Assert.assertEquals((Integer) 33, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestVmwareClusterReturnsVirtualMachineMaxNicsVmwareClusterValue() throws NoSuchFieldException, IllegalAccessException { + ClusterVO clusterVoMock = Mockito.mock(ClusterVO.class); + ConfigKey virtualMachineMaxNicsVmwareMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(1L).when(clusterVoMock).getId(); + Mockito.doReturn(Hypervisor.HypervisorType.VMware).when(clusterVoMock).getHypervisorType(); + Mockito.doReturn(22).when(virtualMachineMaxNicsVmwareMock).valueIn(1L); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsVmware"), virtualMachineMaxNicsVmwareMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(clusterVoMock); + + Assert.assertEquals((Integer) 22, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestXenserverClusterReturnsVirtualMachineMaxNicsXenserverClusterValue() throws NoSuchFieldException, IllegalAccessException { + ClusterVO clusterVoMock = Mockito.mock(ClusterVO.class); + ConfigKey virtualMachineMaxNicsXenserverMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(1L).when(clusterVoMock).getId(); + Mockito.doReturn(Hypervisor.HypervisorType.XenServer).when(clusterVoMock).getHypervisorType(); + Mockito.doReturn(11).when(virtualMachineMaxNicsXenserverMock).valueIn(1L); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsXenserver"), virtualMachineMaxNicsXenserverMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(clusterVoMock); + + Assert.assertEquals((Integer) 11, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromClusterTestIncompatibleHypervisorReturnsNull() { + ClusterVO clusterVoMock = Mockito.mock(ClusterVO.class); + Mockito.doReturn(1L).when(clusterVoMock).getId(); + Mockito.doReturn(Hypervisor.HypervisorType.Hyperv).when(clusterVoMock).getHypervisorType(); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromCluster(clusterVoMock); + + Assert.assertNull(virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromVmHypervisorTypeTestKvmHypervisorReturnsVirtualMachineMaxNicsKvmGlobalValue() throws NoSuchFieldException, IllegalAccessException { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + ConfigKey virtualMachineMaxNicsKvmMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(Hypervisor.HypervisorType.KVM).when(virtualMachineMock).getHypervisorType(); + Mockito.doReturn(23).when(virtualMachineMaxNicsKvmMock).value(); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsKvm"), virtualMachineMaxNicsKvmMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachineMock); + + Assert.assertEquals((Integer) 23, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromVmHypervisorTypeTestVmwareHypervisorReturnsVirtualMachineMaxNicsVmwareGlobalValue() throws NoSuchFieldException, IllegalAccessException { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + ConfigKey virtualMachineMaxNicsVmwareMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(Hypervisor.HypervisorType.VMware).when(virtualMachineMock).getHypervisorType(); + Mockito.doReturn(10).when(virtualMachineMaxNicsVmwareMock).value(); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsVmware"), virtualMachineMaxNicsVmwareMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachineMock); + + Assert.assertEquals((Integer) 10, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromVmHypervisorTypeTestXenserverHypervisorReturnsVirtualMachineMaxNicsXenserverGlobalValue() throws NoSuchFieldException, IllegalAccessException { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + ConfigKey virtualMachineMaxNicsXenserverMock = Mockito.mock(ConfigKey.class); + Mockito.doReturn(Hypervisor.HypervisorType.XenServer).when(virtualMachineMock).getHypervisorType(); + Mockito.doReturn(7).when(virtualMachineMaxNicsXenserverMock).value(); + updateFinalStaticField(testOrchestrator.getClass().getField("VirtualMachineMaxNicsXenserver"), virtualMachineMaxNicsXenserverMock); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachineMock); + + Assert.assertEquals((Integer) 7, virtualMachineMaxNicsValue); + } + + @Test + public void getVirtualMachineMaxNicsValueFromVmHypervisorTypeTestIncompatibleHypervisorReturnsNull() { + VirtualMachine virtualMachineMock = Mockito.mock(VirtualMachine.class); + Mockito.doReturn(Hypervisor.HypervisorType.Hyperv).when(virtualMachineMock).getHypervisorType(); + + Integer virtualMachineMaxNicsValue = testOrchestrator.getVirtualMachineMaxNicsValueFromVmHypervisorType(virtualMachineMock); + + Assert.assertNull(virtualMachineMaxNicsValue); + } + + void updateFinalStaticField(Field field, Object newValue) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, SecurityException { + field.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); + + field.set(null, newValue); + } } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDao.java b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDao.java index 8e965b210beb..1191c4aba348 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDao.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDao.java @@ -165,4 +165,6 @@ public interface DomainRouterDao extends GenericDao { List listStopped(long networkId); List listIncludingRemovedByVpcId(long vpcId); + + DomainRouterVO findOneByVpcId(long vpcId); } diff --git a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java index 63cdc042b26f..b4f04a3c47f3 100644 --- a/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java +++ b/engine/schema/src/main/java/com/cloud/vm/dao/DomainRouterDaoImpl.java @@ -61,6 +61,7 @@ public class DomainRouterDaoImpl extends GenericDaoBase im protected SearchBuilder OutsidePodSearch; protected SearchBuilder clusterSearch; protected SearchBuilder SearchByStateAndManagementServerId; + protected SearchBuilder VpcSingleRouterSearch; @Inject HostDao _hostsDao; @Inject @@ -98,6 +99,12 @@ protected void init() { VpcSearch.and("vpcId", VpcSearch.entity().getVpcId(), Op.EQ); VpcSearch.done(); + VpcSingleRouterSearch = createSearchBuilder(); + VpcSingleRouterSearch.and("role", VpcSingleRouterSearch.entity().getRole(), Op.EQ); + VpcSingleRouterSearch.and("vpcId", VpcSingleRouterSearch.entity().getVpcId(), Op.EQ); + VpcSingleRouterSearch.groupBy(VpcSingleRouterSearch.entity().getVpcId()); + VpcSingleRouterSearch.done(); + IdNetworkIdStatesSearch = createSearchBuilder(); IdNetworkIdStatesSearch.and("id", IdNetworkIdStatesSearch.entity().getId(), Op.EQ); final SearchBuilder joinRouterNetwork1 = _routerNetworkDao.createSearchBuilder(); @@ -450,4 +457,12 @@ public List listIncludingRemovedByVpcId(long vpcId) { sc.setParameters("role", Role.VIRTUAL_ROUTER); return listIncludingRemovedBy(sc); } + + @Override + public DomainRouterVO findOneByVpcId(final long vpcId) { + final SearchCriteria sc = VpcSingleRouterSearch.create(); + sc.setParameters("vpcId", vpcId); + sc.setParameters("role", Role.VIRTUAL_ROUTER); + return findOneBy(sc); + } } diff --git a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDao.java b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDao.java index 509d02dd71c5..feb608797cdc 100644 --- a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDao.java +++ b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDao.java @@ -37,4 +37,6 @@ public interface DomainRouterJoinDao extends GenericDao searchByIds(Long... ids); List getRouterByIdAndTrafficType(Long id, Networks.TrafficType... trafficType); + + int countDefaultNetworksById(long routerId); } diff --git a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java index 2c6c64816619..e8aa29bf908d 100644 --- a/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/DomainRouterJoinDaoImpl.java @@ -21,6 +21,7 @@ import javax.inject.Inject; +import com.cloud.utils.db.GenericSearchBuilder; import org.apache.cloudstack.annotation.AnnotationService; import org.apache.cloudstack.annotation.dao.AnnotationDao; import org.apache.cloudstack.context.CallContext; @@ -59,9 +60,9 @@ public class DomainRouterJoinDaoImpl extends GenericDaoBase vrSearch; - private final SearchBuilder vrIdSearch; private final SearchBuilder vrIdTrafficSearch; + private final GenericSearchBuilder defaultNetworksSearch; protected DomainRouterJoinDaoImpl() { @@ -78,6 +79,13 @@ protected DomainRouterJoinDaoImpl() { vrIdTrafficSearch.and("trafficType", vrIdTrafficSearch.entity().getTrafficType(), SearchCriteria.Op.IN); vrIdTrafficSearch.done(); + defaultNetworksSearch = createSearchBuilder(Integer.class); + defaultNetworksSearch.select(null, SearchCriteria.Func.COUNT, defaultNetworksSearch.entity().getId()); + defaultNetworksSearch.and("id", defaultNetworksSearch.entity().getId(), SearchCriteria.Op.EQ); + defaultNetworksSearch.and("network_name", defaultNetworksSearch.entity().getNetworkName(), SearchCriteria.Op.NULL); + defaultNetworksSearch.and("removed", defaultNetworksSearch.entity().getRemoved(), SearchCriteria.Op.NULL); + defaultNetworksSearch.done(); + _count = "select count(distinct id) from domain_router_view WHERE "; } @@ -347,6 +355,14 @@ public List getRouterByIdAndTrafficType(Long id, TrafficType return searchIncludingRemoved(sc, null, null, false); } + @Override + public int countDefaultNetworksById(long routerId) { + SearchCriteria sc = defaultNetworksSearch.create(); + sc.setParameters("id", routerId); + final List count = customSearch(sc, null); + return count.get(0); + } + @Override public List newDomainRouterView(VirtualRouter vr) { diff --git a/server/src/main/java/com/cloud/configuration/Config.java b/server/src/main/java/com/cloud/configuration/Config.java index 3e039e44c0c4..d3ee60d1f027 100644 --- a/server/src/main/java/com/cloud/configuration/Config.java +++ b/server/src/main/java/com/cloud/configuration/Config.java @@ -1512,7 +1512,6 @@ public enum Config { "3600", "The interval (in seconds) between cleanup for Inactive VPCs", null), - VpcMaxNetworks("Advanced", ManagementServer.class, Integer.class, "vpc.max.networks", "3", "Maximum number of networks per vpc", null), DetailBatchQuerySize("Advanced", ManagementServer.class, Integer.class, "detail.batch.query.size", "2000", "Default entity detail batch query size for listing", null), NetworkIPv6SearchRetryMax( "Network", diff --git a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java index e4219c858da6..60919d98f2cc 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -42,6 +42,7 @@ import javax.inject.Inject; import javax.naming.ConfigurationException; +import com.cloud.api.query.dao.DomainRouterJoinDao; import com.cloud.configuration.ConfigurationManager; import com.cloud.configuration.ConfigurationManagerImpl; import com.cloud.bgp.BGPService; @@ -294,6 +295,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis AlertManager alertManager; @Inject CommandSetupHelper commandSetupHelper; + @Inject + NetworkOrchestrationService networkOrchestrationService; @Autowired @Qualifier("networkHelper") protected NetworkHelper networkHelper; @@ -304,6 +307,8 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis @Inject private VpcPrivateGatewayTransactionCallable vpcTxCallable; @Inject + protected DomainRouterJoinDao domainRouterJoinDao; + @Inject private NsxProviderDao nsxProviderDao; @Inject private NetrisProviderDao netrisProviderDao; @@ -331,7 +336,6 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis Provider.JuniperContrailVpcRouter, Provider.Ovs, Provider.BigSwitchBcf, Provider.ConfigDrive, Provider.Nsx, Provider.Netris); int _cleanupInterval; - int _maxNetworks; SearchBuilder IpAddressSearch; protected final List hTypes = new ArrayList(); @@ -507,9 +511,6 @@ public void doInTransactionWithoutResult(final TransactionStatus status) { final String value = configs.get(Config.VpcCleanupInterval.key()); _cleanupInterval = NumbersUtil.parseInt(value, 60 * 60); // 1 hour - final String maxNtwks = configs.get(Config.VpcMaxNetworks.key()); - _maxNetworks = NumbersUtil.parseInt(maxNtwks, 3); // max=3 is default - IpAddressSearch = _ipAddressDao.createSearchBuilder(); IpAddressSearch.and("accountId", IpAddressSearch.entity().getAllocatedToAccountId(), Op.EQ); IpAddressSearch.and("dataCenterId", IpAddressSearch.entity().getDataCenterId(), Op.EQ); @@ -2185,67 +2186,106 @@ public void validateNtwkOffForVpc(final NetworkOffering guestNtwkOff, final List @DB protected void validateNewVpcGuestNetwork(final String cidr, final String gateway, final Account networkOwner, final Vpc vpc, final String networkDomain) { - Transaction.execute(new TransactionCallbackNoReturn() { @Override public void doInTransactionWithoutResult(final TransactionStatus status) { - final Vpc locked = vpcDao.acquireInLockTable(vpc.getId()); + long vpcId = vpc.getId(); + final Vpc locked = vpcDao.acquireInLockTable(vpcId); if (locked == null) { throw new CloudRuntimeException("Unable to acquire lock on " + vpc); } try { - // check number of active networks in vpc - if (_ntwkDao.countVpcNetworks(vpc.getId()) >= _maxNetworks) { - logger.warn(String.format("Failed to create a new VPC Guest Network because the number of networks per VPC has reached its maximum capacity of [%s]. Increase it by modifying global config [%s].", _maxNetworks, Config.VpcMaxNetworks)); - throw new CloudRuntimeException(String.format("Number of networks per VPC cannot surpass [%s].", _maxNetworks)); - } + Account account = _accountMgr.getAccount(vpc.getAccountId()); + + checkIfVpcNumberOfTiersIsNotExceeded(vpcId, account); + + checkIfVpcHasDomainRouterWithSufficientNicCapacity(vpc); - // 1) CIDR is required if (cidr == null) { throw new InvalidParameterValueException("Gateway/netmask are required when create network for VPC"); } - // 2) Network cidr should be within vpcCidr - if (!NetUtils.isNetworkAWithinNetworkB(cidr, vpc.getCidr())) { - throw new InvalidParameterValueException("Network cidr " + cidr + " is not within vpc " + vpc + " cidr"); - } - - // 3) Network cidr shouldn't cross the cidr of other vpc - // network cidrs - final List ntwks = _ntwkDao.listByVpc(vpc.getId()); - for (final Network ntwk : ntwks) { - assert cidr != null : "Why the network cidr is null when it belongs to vpc?"; + checkIfNetworkCidrIsWithinVpcCidr(cidr, vpc); - if (NetUtils.isNetworkAWithinNetworkB(ntwk.getCidr(), cidr) || NetUtils.isNetworkAWithinNetworkB(cidr, ntwk.getCidr())) { - throw new InvalidParameterValueException("Network cidr " + cidr + " crosses other network cidr " + ntwk + " belonging to the same vpc " + vpc); - } - } + checkIfNetworkCidrNotCrossesOtherVpcNetworksCidr(cidr, vpc); - // 4) Vpc's account should be able to access network owner's account - CheckAccountsAccess(vpc, networkOwner); + checkAccountsAccess(vpc, networkOwner); - // 5) network domain should be the same as VPC's - if (!networkDomain.equalsIgnoreCase(vpc.getNetworkDomain())) { - throw new InvalidParameterValueException("Network domain of the new network should match network" + " domain of vpc " + vpc); - } + checkIfNetworkAndVpcDomainsAreTheSame(networkDomain, vpc); - // 6) gateway should never be equal to the cidr subnet - if (NetUtils.getCidrSubNet(cidr).equalsIgnoreCase(gateway)) { - throw new InvalidParameterValueException("Invalid gateway specified. It should never be equal to the cidr subnet value"); - } + checkIfGatewayIsDifferentFromCidrSubnet(cidr, gateway); } finally { - logger.debug("Releasing lock for " + locked); + logger.debug("Releasing lock for {}.", locked); vpcDao.releaseFromLockTable(locked.getId()); } } }); } - private void CheckAccountsAccess(Vpc vpc, Account networkAccount) { - Account vpcaccount = _accountMgr.getAccount(vpc.getAccountId()); + protected boolean existsVpcDomainRouterWithSufficientNicCapacity(DomainRouterVO vpcDomainRouter) { + int countRouterDefaultNetworks = domainRouterJoinDao.countDefaultNetworksById(vpcDomainRouter.getId()); + long countVpcNetworks = _ntwkDao.countVpcNetworks(vpcDomainRouter.getVpcId()); + + Integer routerMaxNicsValue = networkOrchestrationService.getVirtualMachineMaxNicsValue(vpcDomainRouter); + + if (routerMaxNicsValue == null) { + return true; + } + + int totalNicsAvailable = routerMaxNicsValue - countRouterDefaultNetworks; + + return totalNicsAvailable > countVpcNetworks; + } + + protected void checkIfVpcNumberOfTiersIsNotExceeded(long vpcId, Account account) { + int maxNetworks = VpcMaxNetworks.valueIn(account.getId()); + + if (_ntwkDao.countVpcNetworks(vpcId) >= maxNetworks) { + logger.warn("Failed to create a new VPC Guest Network because the number of networks per VPC has reached its maximum capacity of {}. Increase it by modifying global or account config {}.", maxNetworks, VpcMaxNetworks); + throw new CloudRuntimeException(String.format("Number of networks per VPC cannot surpass [%s] for account [%s].", maxNetworks, account.getAccountName())); + } + } + + protected void checkIfVpcHasDomainRouterWithSufficientNicCapacity(Vpc vpc) { + DomainRouterVO vpcDomainRouter = routerDao.findOneByVpcId(vpc.getId()); + if (vpcDomainRouter == null) { + logger.warn("No virtual router found for VPC {}. Skipping VR NIC capacity check.", vpc.getUuid()); + return; + } + + if (!existsVpcDomainRouterWithSufficientNicCapacity(vpcDomainRouter)) { + logger.warn("Failed to create a new VPC Guest Network because no virtual routers were found with sufficient NIC capacity. The number of VPC Guest networks cannot exceed the number of NICs a virtual router can have."); + throw new CloudRuntimeException(String.format("No available virtual router found to deploy new Guest Network on VPC [%s].", vpc.getName())); + } + } + + protected void checkIfNetworkCidrIsWithinVpcCidr(String cidr, Vpc vpc) { + if (!NetUtils.isNetworkAWithinNetworkB(cidr, vpc.getCidr())) { + throw new InvalidParameterValueException(String.format("Network CIDR %s is not within VPC %s CIDR", cidr, vpc)); + } + } + + protected void checkIfNetworkCidrNotCrossesOtherVpcNetworksCidr(String cidr, Vpc vpc) { + final List networks = _ntwkDao.listByVpc(vpc.getId()); + + for (final Network network : networks) { + if (NetUtils.isNetworkAWithinNetworkB(network.getCidr(), cidr) || NetUtils.isNetworkAWithinNetworkB(cidr, network.getCidr())) { + throw new InvalidParameterValueException(String.format("Network CIDR %s crosses other network CIDR %s belonging to the same VPC %s.", cidr, network, vpc)); + } + } + } + + /** + * Checks if the VPC account has access to the account for which the tier is being created. + * + * @param vpc Vpc to get the account. + * @param networkAccount Tier owner account. + */ + private void checkAccountsAccess(Vpc vpc, Account networkAccount) { + Account vpcAccount = _accountMgr.getAccount(vpc.getAccountId()); try { - _accountMgr.checkAccess(vpcaccount, null, false, networkAccount); + _accountMgr.checkAccess(vpcAccount, null, false, networkAccount); } catch (PermissionDeniedException e) { logger.error(e.getMessage()); @@ -2253,6 +2293,18 @@ private void CheckAccountsAccess(Vpc vpc, Account networkAccount) { } } + protected void checkIfNetworkAndVpcDomainsAreTheSame(String networkDomain, Vpc vpc) { + if (!networkDomain.equalsIgnoreCase(vpc.getNetworkDomain())) { + throw new InvalidParameterValueException(String.format("Network domain of the new network should match VPC domain [%s].", vpc)); + } + } + + protected void checkIfGatewayIsDifferentFromCidrSubnet(String cidr, String gateway) { + if (NetUtils.getCidrSubNet(cidr).equalsIgnoreCase(gateway)) { + throw new InvalidParameterValueException("Invalid gateway specified. It should never be equal to the CIDR subnet value."); + } + } + public List getVpcElements() { if (vpcElements == null) { vpcElements = new ArrayList(); @@ -3308,7 +3360,8 @@ public String getConfigComponentName() { public ConfigKey[] getConfigKeys() { return new ConfigKey[]{ VpcTierNamePrepend, - VpcTierNamePrependDelimiter + VpcTierNamePrependDelimiter, + VpcMaxNetworks }; } diff --git a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java index 4f92c60e25ad..ff28af5b29eb 100644 --- a/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java +++ b/server/src/test/java/com/cloud/network/vpc/VpcManagerImplTest.java @@ -23,6 +23,7 @@ import com.cloud.agent.api.to.IpAddressTO; import com.cloud.agent.manager.Commands; import com.cloud.alert.AlertManager; +import com.cloud.api.query.dao.DomainRouterJoinDao; import com.cloud.configuration.Resource; import com.cloud.dc.DataCenterVO; import com.cloud.dc.VlanVO; @@ -62,6 +63,7 @@ import com.cloud.user.UserVO; import com.cloud.utils.Pair; import com.cloud.utils.db.EntityManager; +import com.cloud.utils.exception.CloudRuntimeException; import com.cloud.utils.net.Ip; import com.cloud.utils.net.NetUtils; import com.cloud.vm.DomainRouterVO; @@ -83,6 +85,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.test.util.ReflectionTestUtils; @@ -130,6 +133,7 @@ public class VpcManagerImplTest { DataCenterDao dataCenterDao; @Mock VpcOfferingServiceMapDao vpcOfferingServiceMapDao; + @Spy VpcManagerImpl manager; @Mock EntityManager entityMgr; @@ -164,6 +168,14 @@ public class VpcManagerImplTest { @Mock NetworkACLVO networkACLVOMock; @Mock + NetworkOrchestrationService networkOrchestrationServiceMock; + @Mock + DomainRouterVO domainRouterVOMock; + @Mock + Vpc vpcMock; + @Mock + DomainRouterJoinDao domainRouterJoinDaoMock; + @Mock RoutedIpv4Manager routedIpv4Manager; public static final long ACCOUNT_ID = 1; @@ -202,7 +214,6 @@ private void registerCallContext() { @Before public void setup() throws NoSuchFieldException, IllegalAccessException { closeable = MockitoAnnotations.openMocks(this); - manager = new VpcManagerImpl(); manager._vpcOffSvcMapDao = vpcOfferingServiceMapDao; manager.vpcDao = vpcDao; manager._ntwkMgr = networkMgr; @@ -224,6 +235,8 @@ public void setup() throws NoSuchFieldException, IllegalAccessException { manager._ntwkSvc = networkServiceMock; manager._firewallDao = firewallDao; manager._networkAclDao = networkACLDaoMock; + manager.networkOrchestrationService = networkOrchestrationServiceMock; + manager.domainRouterJoinDao = domainRouterJoinDaoMock; manager.routedIpv4Manager = routedIpv4Manager; CallContext.register(Mockito.mock(User.class), Mockito.mock(Account.class)); registerCallContext(); @@ -349,7 +362,6 @@ protected Set prepareVpcManagerForCheckingCapabilityPerService @Test public void testCreateVpcNetwork() throws InsufficientCapacityException, ResourceAllocationException { final long VPC_ID = 201L; - manager._maxNetworks = 3; VpcVO vpcMockVO = Mockito.mock(VpcVO.class); Vpc vpcMock = Mockito.mock(Vpc.class); Account accountMock = Mockito.mock(Account.class); @@ -365,12 +377,12 @@ public void testCreateVpcNetwork() throws InsufficientCapacityException, Resourc Mockito.when(entityMgr.findById(NetworkOffering.class, 1L)).thenReturn(offering); Mockito.when(vpcMock.getId()).thenReturn(VPC_ID); Mockito.when(vpcDao.acquireInLockTable(VPC_ID)).thenReturn(vpcMockVO); - Mockito.when(networkDao.countVpcNetworks(anyLong())).thenReturn(1L); Mockito.when(offering.getGuestType()).thenReturn(Network.GuestType.Isolated); Mockito.when(networkModel.listNetworkOfferingServices(anyLong())).thenReturn(services); Mockito.when(networkOfferingServiceMapDao.listByNetworkOfferingId(anyLong())).thenReturn(serviceMap); Mockito.when(vpcMock.getCidr()).thenReturn("10.0.0.0/8"); Mockito.when(vpcMock.getNetworkDomain()).thenReturn("cs1cloud.internal"); + Mockito.doNothing().when(manager).checkIfVpcNumberOfTiersIsNotExceeded(Mockito.anyLong(), Mockito.any()); manager.validateNewVpcGuestNetwork("10.10.10.0/24", "10.10.10.1", accountMock, vpcMock, "cs1cloud.internal"); manager.validateNtwkOffForNtwkInVpc(2L, 1, "10.10.10.0/24", "111-", vpcMock, "10.1.1.1", new AccountVO(), null); @@ -581,4 +593,98 @@ public void validateVpcPrivateGatewayTestAclFromDifferentVpcThrowsInvalidParamet Assert.assertThrows(InvalidParameterValueException.class, () -> manager.validateVpcPrivateGatewayAclId(vpcId, differentVpcAclId)); } + @Test + public void existsVpcDomainRouterWithSufficientNicCapacityTestUnavailableRoutersReturnsFalse() { + Mockito.when(networkDao.countVpcNetworks(vpcId)).thenReturn(7L); + Mockito.when(domainRouterVOMock.getId()).thenReturn(1L); + Mockito.when(domainRouterVOMock.getVpcId()).thenReturn(vpcId); + Mockito.when(domainRouterJoinDaoMock.countDefaultNetworksById(1L)).thenReturn(2); + Mockito.when(networkOrchestrationServiceMock.getVirtualMachineMaxNicsValue(domainRouterVOMock)).thenReturn(9); + + boolean result = manager.existsVpcDomainRouterWithSufficientNicCapacity(domainRouterVOMock); + + Assert.assertFalse(result); + } + + @Test + public void existsVpcDomainRouterWithSufficientNicCapacityTestRouterIncompatibleHypervisorTypeReturnsTrue() { + Mockito.when(domainRouterVOMock.getId()).thenReturn(1L); + Mockito.when(domainRouterJoinDaoMock.countDefaultNetworksById(1L)).thenReturn(2); + Mockito.when(networkOrchestrationServiceMock.getVirtualMachineMaxNicsValue(domainRouterVOMock)).thenReturn(null); + + boolean result = manager.existsVpcDomainRouterWithSufficientNicCapacity(domainRouterVOMock); + + Assert.assertTrue(result); + } + + @Test + public void existsVpcDomainRouterWithSufficientNicCapacityTestAvailableRouterReturnsTrue() { + Mockito.when(domainRouterVOMock.getId()).thenReturn(1L); + Mockito.when(domainRouterJoinDaoMock.countDefaultNetworksById(1L)).thenReturn(2); + Mockito.when(networkOrchestrationServiceMock.getVirtualMachineMaxNicsValue(domainRouterVOMock)).thenReturn(9); + + boolean result = manager.existsVpcDomainRouterWithSufficientNicCapacity(domainRouterVOMock); + + Assert.assertTrue(result); + } + + @Test + public void existsVpcDomainRouterWithSufficientNicCapacityTestNullRouterReturnsFalse() { + boolean result = manager.existsVpcDomainRouterWithSufficientNicCapacity(domainRouterVOMock); + + Assert.assertFalse(result); + } + + @Test + public void checkIfVpcNumberOfTiersIsNotExceededTestExceededTiersThrowCloudRuntimeException() { + AccountVO accountMock = Mockito.mock(AccountVO.class); + Mockito.doReturn(5L).when(networkDao).countVpcNetworks(1L); + + Assert.assertThrows(CloudRuntimeException.class, () -> manager.checkIfVpcNumberOfTiersIsNotExceeded(vpcId, accountMock)); + } + + @Test + public void checkIfVpcHasDomainRouterWithSufficientNicCapacityTestDomainRoutersWithoutCapacityThrowsCloudRuntimeException() { + Mockito.doReturn(vpcId).when(vpcMock).getId(); + Mockito.doReturn(domainRouterVOMock).when(routerDao).findOneByVpcId(vpcId); + Mockito.doReturn(false).when(manager).existsVpcDomainRouterWithSufficientNicCapacity(domainRouterVOMock); + + Assert.assertThrows(CloudRuntimeException.class, () -> manager.checkIfVpcHasDomainRouterWithSufficientNicCapacity(vpcMock)); + } + + @Test + public void checkIfNetworkCidrIsWithinVpcCidrTestNetworkCidrOutsideOfVpcCidrThrowInvalidParameterValueException() { + String cidr = "10.0.0.1/24"; + Mockito.doReturn("192.168.0.0/24").when(vpcMock).getCidr(); + + Assert.assertThrows(InvalidParameterValueException.class, () -> manager.checkIfNetworkCidrIsWithinVpcCidr(cidr, vpcMock)); + } + + @Test + public void checkIfNetworkCidrNotCrossesOtherVpcNetworksCidrNetworkCidrCrossesOtherVpcNetworkCidr() { + String cidr = "192.168.0.0/24"; + Network networkMock = Mockito.mock(Network.class); + Mockito.doReturn("192.168.0.0/24").when(networkMock).getCidr(); + Mockito.doReturn(vpcId).when(vpcMock).getId(); + Mockito.doReturn(List.of(networkMock)).when(networkDao).listByVpc(vpcId); + + Assert.assertThrows(InvalidParameterValueException.class, () -> manager.checkIfNetworkCidrNotCrossesOtherVpcNetworksCidr(cidr, vpcMock)); + } + + @Test + public void checkIfNetworkAndVpcDomainsAreTheSameTestDifferentDomainsThrowInvalidParameterValueException() { + String domain = "domain"; + Mockito.doReturn("anotherDomain").when(vpcMock).getNetworkDomain(); + + Assert.assertThrows(InvalidParameterValueException.class, () -> manager.checkIfNetworkAndVpcDomainsAreTheSame(domain, vpcMock)); + } + + @Test + public void checkIfGatewayIsDifferentFromCidrSubnetTestGatewayEqualsCidrSubnetThrowInvalidParameterValueException() { + String gateway = "192.168.0.0"; + String cidr = "192.168.0.1/24"; + + Assert.assertThrows(InvalidParameterValueException.class, () -> manager.checkIfGatewayIsDifferentFromCidrSubnet(cidr, gateway)); + } + } diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 931206737206..2216c0ae94a1 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -1064,6 +1064,11 @@ public Pair importNic(String macAddress, int deviceId, Netw public void unmanageNics(VirtualMachineProfile vm) { } + @Override + public Integer getVirtualMachineMaxNicsValue(VirtualMachine virtualMachine) { + return 0; + } + @Override public Pair, Integer> listGuestVlans(ListGuestVlansCmd cmd) { return null; diff --git a/test/integration/component/test_vpc_vms_deployment.py b/test/integration/component/test_vpc_vms_deployment.py index 4d3f93471d1a..6f74a3c23623 100644 --- a/test/integration/component/test_vpc_vms_deployment.py +++ b/test/integration/component/test_vpc_vms_deployment.py @@ -123,8 +123,7 @@ def __init__(self): "displaytext": "Test Network", "netmask": '255.255.255.0', "limit": 5, - # Max networks allowed as per hypervisor - # Xenserver -> 5, VMWare -> 9 + # Max networks allowed per VPC }, "natrule": { "privateport": 22, @@ -1344,8 +1343,8 @@ def test_04_deploy_vms_delete_add_network_noLb(self): return @attr(tags=["advanced", "intervlan"], required_hardware="false") - def test_05_create_network_max_limit(self): - """ Test create networks in VPC upto maximum limit for hypervisor + def test_05_create_network_max_limit_vpc(self): + """ Test create networks in VPC up to maximum limit for VPC. """ # Validate the following diff --git a/test/integration/smoke/test_vpc_hypervisor_max_networks.py b/test/integration/smoke/test_vpc_hypervisor_max_networks.py new file mode 100644 index 000000000000..9618d1ac18f0 --- /dev/null +++ b/test/integration/smoke/test_vpc_hypervisor_max_networks.py @@ -0,0 +1,391 @@ +# 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. + +""" Test VPC max networks based on the hypervisor limits """ + +from nose.plugins.attrib import attr +from marvin.cloudstackTestCase import cloudstackTestCase +from marvin.lib.base import (Account, + NetworkOffering, + Network, + VPC, + VpcOffering, + Router, + Host, + Cluster, + Configurations, + Resources) +from marvin.lib.common import (get_zone, + get_domain) + + +class Services: + def __init__(self): + self.services = { + "account": { + "email": "test@test.com", + "firstname": "Test", + "lastname": "User", + "username": "test", + # Random characters are appended for unique + # username + "password": "password", + }, + "network_offering": { + "name": 'VPC Network offering', + "displaytext": 'VPC Network off', + "guestiptype": 'Isolated', + "supportedservices": 'Vpn,Dhcp,Dns,SourceNat,PortForwarding,Lb,UserData,StaticNat,NetworkACL', + "traffictype": 'GUEST', + "availability": 'Optional', + "useVpc": 'on', + "serviceProviderList": { + "Vpn": 'VpcVirtualRouter', + "Dhcp": 'VpcVirtualRouter', + "Dns": 'VpcVirtualRouter', + "SourceNat": 'VpcVirtualRouter', + "PortForwarding": 'VpcVirtualRouter', + "Lb": 'VpcVirtualRouter', + "UserData": 'VpcVirtualRouter', + "StaticNat": 'VpcVirtualRouter', + "NetworkACL": 'VpcVirtualRouter' + }, + "serviceCapabilityList": { + "SourceNat": {"SupportedSourceNatTypes": "peraccount"}, + }, + }, + "vpc_offering": { + "name": 'VPC off', + "displaytext": 'VPC off', + "supportedservices": 'Dhcp,Dns,SourceNat,PortForwarding,Vpn,Lb,UserData,StaticNat', + }, + "vpc": { + "name": "TestVPC", + "displaytext": "TestVPC", + "cidr": '10.0.0.1/24' + }, + "network": { + "name": "Test Network", + "displaytext": "Test Network", + "netmask": '255.255.255.0', + # Max networks allowed per hypervisor: Xenserver -> 7, VMWare -> 10 & KVM -> 27 + "limit": 5, + } + } + + +class TestVpcHypervisorMaxNetworks(cloudstackTestCase): + @classmethod + def setUpClass(cls): + cls.testClient = super(TestVpcHypervisorMaxNetworks, cls).getClsTestClient() + cls.api_client = cls.testClient.getApiClient() + + cls.services = Services().services + + cls.domain = get_domain(cls.api_client) + cls.zone = get_zone(cls.api_client, cls.testClient.getZoneForTests()) + + cls._cleanup = [] + + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.account = Account.create( + self.apiclient, + self.services["account"], + admin=True, + domainid=self.domain.id + ) + self.cleanup = [self.account] + + return + + @classmethod + def tearDownClass(cls): + super(TestVpcHypervisorMaxNetworks, cls).tearDownClass() + + def tearDown(self): + super(TestVpcHypervisorMaxNetworks, self).tearDown() + + def validate_vpc_network(self, network, state=None): + """Validates the VPC network""" + + self.debug("Checking if the VPC network was created successfully.") + vpc_networks = VPC.list( + self.apiclient, + id=network.id + ) + self.assertEqual( + isinstance(vpc_networks, list), + True, + "List VPC network should return a valid list" + ) + self.assertEqual( + network.name, + vpc_networks[0].name, + "Name of the VPC network should match with listVPC data" + ) + if state: + self.assertEqual( + vpc_networks[0].state, + state, + "VPC state should be '%s'" % state + ) + self.debug("VPC network validated - %s" % network.name) + + return + + def validate_vpc_offering(self, vpc_offering): + """Validates the VPC offering""" + + self.debug("Checking if the VPC offering was created successfully.") + vpc_offs = VpcOffering.list( + self.apiclient, + id=vpc_offering.id + ) + self.assertEqual( + isinstance(vpc_offs, list), + True, + "List VPC offerings should return a valid list" + ) + self.assertEqual( + vpc_offering.name, + vpc_offs[0].name, + "Name of the VPC offering should match with listVPCOff data" + ) + self.debug("VPC offering was created successfully - %s" % vpc_offering.name) + + return + + def create_network_max_limit_hypervisor_test(self, hypervisor): + Configurations.update( + self.apiclient, + accountid=self.account.id, + name="vpc.max.networks", + value=30 + ) + + self.debug("Creating a VPC offering.") + vpc_off = VpcOffering.create( + self.apiclient, + self.services["vpc_offering"] + ) + + self._cleanup.append(vpc_off) + self.validate_vpc_offering(vpc_off) + + self.debug("Enabling the VPC offering.") + vpc_off.update(self.apiclient, state='Enabled') + + self.debug("Creating a VPC network in the account: %s" % self.account.name) + self.services["vpc"]["cidr"] = '10.1.1.1/16' + vpc = VPC.create( + self.apiclient, + self.services["vpc"], + vpcofferingid=vpc_off.id, + zoneid=self.zone.id, + account=self.account.name, + domainid=self.account.domainid + ) + self.validate_vpc_network(vpc) + + vpc_router = Router.list( + self.apiclient, + vpcId=vpc.id, + listAll=True + )[0] + + host = Host.list( + self.apiclient, + id=vpc_router.hostId + )[0] + + cluster = Cluster.list( + self.apiclient, + id=host.clusterId + )[0] + + cluster_old_hypervisor_type = cluster.hypervisortype + + self.debug("Updating cluster %s hypervisor from %s to %s." % + (cluster.name, cluster_old_hypervisor_type, hypervisor)) + Cluster.update( + self.apiclient, + id=cluster.id, + hypervisor=hypervisor + ) + + network_offering = NetworkOffering.create( + self.apiclient, + self.services["network_offering"], + conservemode=False + ) + # Enable Network offering + self._cleanup.append(network_offering) + network_offering.update(self.apiclient, state='Enabled') + + # Empty list to store all networks + networks = [] + + # Creating network using the network offering created + self.debug("Creating network with network offering: %s." % network_offering.id) + network_1 = Network.create( + self.apiclient, + self.services["network"], + accountid=self.account.name, + domainid=self.account.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + gateway='10.1.0.1', + vpcid=vpc.id + ) + + self.debug("Created network with ID: %s." % network_1.id) + + config_name = "virtual.machine.max.nics.%s" % hypervisor.lower() + + configs = Configurations.list( + self.apiclient, + name=config_name, + listall=True + ) + if not isinstance(configs, list): + raise Exception("Failed to find %s virtual machine max NICs." % hypervisor) + + self.services["network"]["limit"] = int(configs[0].value) + + self.debug("Updating network resource limit for account %s with value %s." % ( + self.account.name, self.services["network"]["limit"])) + Resources.updateLimit( + self.apiclient, + resourcetype=6, + account=self.account.name, + domainid=self.domain.id, + max=self.services["network"]["limit"] + ) + + # Create networks till max limit of hypervisor + for i in range(self.services["network"]["limit"] - 3): + # Creating network using the network offering created + self.debug("Creating network with network offering: %s." % + network_offering.id) + gateway = '10.1.' + str(i + 1) + '.1' + self.debug("Gateway for new network: %s." % gateway) + + network = Network.create( + self.apiclient, + self.services["network"], + accountid=self.account.name, + domainid=self.account.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + gateway=gateway, + vpcid=vpc.id + ) + self.debug("Created network with ID: %s." % network.id) + networks.append(network) + + self.debug( + "Trying to create one more network than %s hypervisor limit in VPC: %s." % (hypervisor, vpc.name)) + gateway = '10.1.' + str(self.services["network"]["limit"]) + '.1' + + with self.assertRaises(Exception): + Network.create( + self.apiclient, + self.services["network"], + accountid=self.account.name, + domainid=self.account.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + gateway=gateway, + vpcid=vpc.id + ) + + self.debug("Deleting one of the existing networks.") + try: + network_1.delete(self.apiclient) + except Exception as e: + self.fail("Failed to delete network: %s - %s." % + (network_1.name, e)) + + self.debug("Creating a new network in VPC: %s." % vpc.name) + network = Network.create( + self.apiclient, + self.services["network"], + accountid=self.account.name, + domainid=self.account.domainid, + networkofferingid=network_offering.id, + zoneid=self.zone.id, + gateway=gateway, + vpcid=vpc.id + ) + self.debug("Created a new network: %s." % network.name) + networks.append(network) + + self.debug( + "Updating cluster %s hypervisor to its old hypervisor %s." % (cluster.name, cluster_old_hypervisor_type)) + Cluster.update( + self.apiclient, + id=cluster.id, + hypervisor=cluster_old_hypervisor_type + ) + + return + + @attr(tags=["advanced", "intervlan"], required_hardware="true") + def test_01_create_network_max_limit_xenserver(self): + """ Test create networks in VPC up to maximum limit for XenServer hypervisor. + """ + + # Validate the following + # 1. Create a VPC and add maximum # of supported networks to the VPC; + # 2. After reaching the maximum networks, adding another network to the VPC must raise an exception; + # 3. Delete a network from VPC; + # 4. Add a new network to the VPC. + + self.create_network_max_limit_hypervisor_test("XenServer") + return + + @attr(tags=["advanced", "intervlan"], required_hardware="true") + def test_02_create_network_max_limit_vmware(self): + """ Test create networks in VPC up to maximum limit for VMware hypervisor. + """ + + # Validate the following + # 1. Create a VPC and add maximum # of supported networks to the VPC; + # 2. After reaching the maximum networks, adding another network to the VPC must raise an exception; + # 3. Delete a network from VPC; + # 4. Add a new network to the VPC. + + self.create_network_max_limit_hypervisor_test("VMware") + return + + @attr(tags=["advanced", "intervlan"], required_hardware="true") + def test_03_create_network_max_limit_kvm(self): + """ Test create networks in VPC up to maximum limit for KVM hypervisor. + """ + + # Validate the following + # 1. Create a VPC and add maximum # of supported networks to the VPC; + # 2. After reaching the maximum networks, adding another network to the VPC must raise an exception; + # 3. Delete a network from VPC; + # 4. Add a new network to the VPC. + + self.create_network_max_limit_hypervisor_test("KVM") + return