From aa01580381c931512d62edbc658865f776ce2efa Mon Sep 17 00:00:00 2001 From: Pearl Dsilva Date: Thu, 18 Feb 2021 13:54:09 +0530 Subject: [PATCH] network: Specify IP for VR in shared networks (#4503) This PR enables admins to specify IP for a VR in a shared network. --- .../main/java/com/cloud/network/Network.java | 4 + .../com/cloud/network/NetworkProfile.java | 10 + .../apache/cloudstack/api/ApiConstants.java | 2 + .../network/CreateNetworkCmdByAdmin.java | 17 ++ .../service/NetworkOrchestrationService.java | 2 +- .../orchestration/NetworkOrchestrator.java | 28 ++- .../java/com/cloud/network/dao/NetworkVO.java | 22 ++ .../cluster/KubernetesClusterManagerImpl.java | 2 +- .../cloud/network/IpAddressManagerImpl.java | 12 +- .../com/cloud/network/NetworkServiceImpl.java | 61 +++++- .../cloud/network/guru/DirectNetworkGuru.java | 7 + .../network/router/NetworkHelperImpl.java | 43 ++-- .../com/cloud/network/vpc/VpcManagerImpl.java | 2 +- .../java/com/cloud/vm/UserVmManagerImpl.java | 4 +- .../network/CreatePrivateNetworkTest.java | 2 +- .../com/cloud/vpc/MockNetworkManagerImpl.java | 2 +- test/integration/smoke/test_network.py | 197 +++++++++++++++++- tools/marvin/marvin/lib/base.py | 12 ++ ui/public/locales/en.json | 2 + .../views/network/CreateSharedNetworkForm.vue | 55 ++++- 20 files changed, 452 insertions(+), 34 deletions(-) diff --git a/api/src/main/java/com/cloud/network/Network.java b/api/src/main/java/com/cloud/network/Network.java index 599bfeec49e..37b8f332d0a 100644 --- a/api/src/main/java/com/cloud/network/Network.java +++ b/api/src/main/java/com/cloud/network/Network.java @@ -452,4 +452,8 @@ public interface Network extends ControlledEntity, StateObject, I String getExternalId(); PVlanType getPvlanType(); + + String getRouterIp(); + + String getRouterIpv6(); } diff --git a/api/src/main/java/com/cloud/network/NetworkProfile.java b/api/src/main/java/com/cloud/network/NetworkProfile.java index 117f90e62a4..08a420a1b20 100644 --- a/api/src/main/java/com/cloud/network/NetworkProfile.java +++ b/api/src/main/java/com/cloud/network/NetworkProfile.java @@ -319,4 +319,14 @@ public class NetworkProfile implements Network { return null; } + @Override + public String getRouterIp() { + return null; + } + + @Override + public String getRouterIpv6() { + return null; + } + } diff --git a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java index 0087fee3340..5c3050c85ae 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -802,6 +802,8 @@ public class ApiConstants { public static final String ROUTER_HEALTH_CHECKS = "healthchecks"; public static final String ROUTER_CHECK_NAME = "checkname"; public static final String ROUTER_CHECK_TYPE = "checktype"; + public static final String ROUTER_IP = "routerip"; + public static final String ROUTER_IPV6 = "routeripv6"; public static final String LAST_UPDATED = "lastupdated"; public static final String PERFORM_FRESH_CHECKS = "performfreshchecks"; public static final String CACHE_MODE = "cachemode"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkCmdByAdmin.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkCmdByAdmin.java index 1f32f62812e..53b02718ea3 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkCmdByAdmin.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkCmdByAdmin.java @@ -16,6 +16,7 @@ // under the License. package org.apache.cloudstack.api.command.admin.network; +import org.apache.cloudstack.api.ApiArgValidator; import org.apache.log4j.Logger; import org.apache.cloudstack.api.APICommand; @@ -42,6 +43,14 @@ public class CreateNetworkCmdByAdmin extends CreateNetworkCmd implements AdminCm @Parameter(name=ApiConstants.HIDE_IP_ADDRESS_USAGE, type=CommandType.BOOLEAN, description="when true ip address usage for the network will not be exported by the listUsageRecords API") private Boolean hideIpAddressUsage; + @Parameter(name = ApiConstants.ROUTER_IP, type = CommandType.STRING, description = "IPV4 address to be assigned to a router in a shared network", since = "4.16", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String routerIp; + + @Parameter(name = ApiConstants.ROUTER_IPV6, type = CommandType.STRING, description = "IPV6 address to be assigned to a router in a shared network", since = "4.16", + validations = {ApiArgValidator.NotNullOrEmpty}) + private String routerIpv6; + ///////////////////////////////////////////////////// /////////////////// Accessors /////////////////////// ///////////////////////////////////////////////////// @@ -63,4 +72,12 @@ public class CreateNetworkCmdByAdmin extends CreateNetworkCmd implements AdminCm } return false; } + + public String getRouterIp() { + return routerIp; + } + + public String getRouterIpv6() { + return routerIpv6; + } } 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 06c3a946ce8..15e44d9ccbb 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 @@ -182,7 +182,7 @@ public interface NetworkOrchestrationService { Network createGuestNetwork(long networkOfferingId, String name, String displayText, String gateway, String cidr, String vlanId, boolean bypassVlanOverlapCheck, String networkDomain, Account owner, Long domainId, PhysicalNetwork physicalNetwork, long zoneId, ACLType aclType, Boolean subdomainAccess, Long vpcId, String ip6Gateway, String ip6Cidr, - Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException; + Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException; UserDataServiceProvider getPasswordResetProvider(Network 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 130e4f0c64a..fbdd166d7cc 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 @@ -623,6 +623,15 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra setStateMachine(); } + private void updateRouterIpInNetworkDetails(Long networkId, String routerIp, String routerIpv6) { + if (isNotBlank(routerIp)) { + networkDetailsDao.addDetail(networkId, ApiConstants.ROUTER_IP, routerIp, true); + } + if (isNotBlank(routerIpv6)) { + networkDetailsDao.addDetail(networkId, ApiConstants.ROUTER_IPV6, routerIpv6, true); + } + } + @Override public List setupNetwork(final Account owner, final NetworkOffering offering, final DeploymentPlan plan, final String name, final String displayText, final boolean isDefault) throws ConcurrentOperationException { @@ -705,6 +714,8 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra networkDetailsDao.persist(detailVO); } + updateRouterIpInNetworkDetails(networkPersisted.getId(), network.getRouterIp(), network.getRouterIpv6()); + if (predefined instanceof NetworkVO && guru instanceof NetworkGuruAdditionalFunctions){ final NetworkGuruAdditionalFunctions functions = (NetworkGuruAdditionalFunctions) guru; functions.finalizeNetworkDesign(networkPersisted.getId(), ((NetworkVO)predefined).getVlanIdAsUUID()); @@ -2297,7 +2308,7 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra // create network for private gateway return createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, null, owner, null, pNtwk, pNtwk.getDataCenterId(), ACLType.Account, null, - vpcId, null, null, true, null, null, null, true); + vpcId, null, null, true, null, null, null, true, null, null); } @Override @@ -2305,18 +2316,18 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra public Network createGuestNetwork(final long networkOfferingId, final String name, final String displayText, final String gateway, final String cidr, String vlanId, boolean bypassVlanOverlapCheck, String networkDomain, final Account owner, final Long domainId, final PhysicalNetwork pNtwk, final long zoneId, final ACLType aclType, Boolean subdomainAccess, final Long vpcId, final String ip6Gateway, final String ip6Cidr, - final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { + final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { // create Isolated/Shared/L2 network return createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, networkDomain, owner, domainId, pNtwk, zoneId, aclType, subdomainAccess, vpcId, ip6Gateway, ip6Cidr, - isDisplayNetworkEnabled, isolatedPvlan, isolatedPvlanType, externalId, false); + isDisplayNetworkEnabled, isolatedPvlan, isolatedPvlanType, externalId, false, routerIp, routerIpv6); } @DB private Network createGuestNetwork(final long networkOfferingId, final String name, final String displayText, final String gateway, final String cidr, String vlanId, boolean bypassVlanOverlapCheck, String networkDomain, final Account owner, final Long domainId, final PhysicalNetwork pNtwk, final long zoneId, final ACLType aclType, Boolean subdomainAccess, final Long vpcId, final String ip6Gateway, final String ip6Cidr, - final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, final Boolean isPrivateNetwork) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { + final Boolean isDisplayNetworkEnabled, final String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, final Boolean isPrivateNetwork, String routerIp, String routerIpv6) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { final NetworkOfferingVO ntwkOff = _networkOfferingDao.findById(networkOfferingId); final DataCenterVO zone = _dcDao.findById(zoneId); @@ -2577,6 +2588,14 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra userNetwork.setExternalId(externalId); } + if (isNotBlank(routerIp)) { + userNetwork.setRouterIp(routerIp); + } + + if (isNotBlank(routerIpv6)) { + userNetwork.setRouterIpv6(routerIpv6); + } + if (vlanIdFinal != null) { if (isolatedPvlan == null) { URI uri = null; @@ -2616,7 +2635,6 @@ public class NetworkOrchestrator extends ManagerBase implements NetworkOrchestra final List networks = setupNetwork(owner, ntwkOff, userNetwork, plan, name, displayText, true, domainId, aclType, subdomainAccessFinal, vpcId, isDisplayNetworkEnabled); - Network network = null; if (networks == null || networks.isEmpty()) { throw new CloudRuntimeException("Fail to create a network"); diff --git a/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java b/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java index 6d591675f82..790b4845bb3 100644 --- a/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java +++ b/engine/schema/src/main/java/com/cloud/network/dao/NetworkVO.java @@ -175,6 +175,12 @@ public class NetworkVO implements Network { @Column(name = "external_id") String externalId; + @Transient + String routerIp; + + @Transient + String routerIpv6; + @Transient transient String vlanIdAsUUID; @@ -672,4 +678,20 @@ public class NetworkVO implements Network { public void setPvlanType(PVlanType pvlanType) { this.pVlanType = pvlanType; } + + public String getRouterIp() { + return routerIp; + } + + public void setRouterIp(String routerIp) { + this.routerIp = routerIp; + } + + public String getRouterIpv6() { + return routerIpv6; + } + + public void setRouterIpv6(String routerIpv6) { + this.routerIpv6 = routerIpv6; + } } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java index 8f124f1e1fa..f091d071690 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/KubernetesClusterManagerImpl.java @@ -797,7 +797,7 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne try { network = networkMgr.createGuestNetwork(networkOffering.getId(), clusterName + "-network", owner.getAccountName() + "-network", - null, null, null, false, null, owner, null, physicalNetwork, zone.getId(), ControlledEntity.ACLType.Account, null, null, null, null, true, null, null, null); + null, null, null, false, null, owner, null, physicalNetwork, zone.getId(), ControlledEntity.ACLType.Account, null, null, null, null, true, null, null, null, null, null); } catch (ConcurrentOperationException | InsufficientCapacityException | ResourceAllocationException e) { logAndThrow(Level.ERROR, String.format("Unable to create network for the Kubernetes cluster: %s", clusterName)); } diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index e5f06c69f3e..51af56a887a 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -32,6 +32,7 @@ import javax.inject.Inject; import org.apache.cloudstack.acl.ControlledEntity.ACLType; import org.apache.cloudstack.acl.SecurityChecker.AccessType; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.response.AcquirePodIpCmdResponse; import org.apache.cloudstack.context.CallContext; import org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService; @@ -850,10 +851,16 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage errorMessage.append(", network id=" + guestNetworkId); } sc.setJoinParameters("vlan", "type", vlanUse); - + String routerIpAddress = null; + if (network != null) { + NetworkDetailVO routerIpDetail = _networkDetailsDao.findDetail(network.getId(), ApiConstants.ROUTER_IP); + routerIpAddress = routerIpDetail != null ? routerIpDetail.getValue() : null; + } if (requestedIp != null) { sc.addAnd("address", SearchCriteria.Op.EQ, requestedIp); errorMessage.append(": requested ip " + requestedIp + " is not available"); + } else if (routerIpAddress != null) { + sc.addAnd("address", Op.NEQ, routerIpAddress); } boolean ascOrder = ! forSystemVms; @@ -1729,7 +1736,7 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage s_logger.debug("Creating network for account " + owner + " from the network offering id=" + requiredOfferings.get(0).getId() + " as a part of createVlanIpRange process"); guestNetwork = _networkMgr.createGuestNetwork(requiredOfferings.get(0).getId(), owner.getAccountName() + "-network", owner.getAccountName() - + "-network", null, null, null, false, null, owner, null, physicalNetwork, zoneId, ACLType.Account, null, null, null, null, true, null, null, null); + + "-network", null, null, null, false, null, owner, null, physicalNetwork, zoneId, ACLType.Account, null, null, null, null, true, null, null, null, null, null); if (guestNetwork == null) { s_logger.warn("Failed to create default Virtual network for the account " + accountId + "in zone " + zoneId); throw new CloudRuntimeException("Failed to create a Guest Isolated Networks with SourceNAT " @@ -2104,7 +2111,6 @@ public class IpAddressManagerImpl extends ManagerBase implements IpAddressManage public void doInTransactionWithoutResult(TransactionStatus status) throws InsufficientAddressCapacityException { //This method allocates direct ip for the Shared network in Advance zones boolean ipv4 = false; - if (network.getGateway() != null) { if (nic.getIPv4Address() == null) { PublicIp ip = null; diff --git a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java index 4a4603c7baa..21d07243aaa 100644 --- a/server/src/main/java/com/cloud/network/NetworkServiceImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkServiceImpl.java @@ -1029,6 +1029,46 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C } } + private void validateRouterIps(String routerIp, String routerIpv6, String startIp, String endIp, String gateway, + String netmask, String startIpv6, String endIpv6, String ip6Cidr) { + if (isNotBlank(routerIp)) { + if (startIp != null && endIp == null) { + endIp = startIp; + } + if (!NetUtils.isValidIp4(routerIp)) { + throw new CloudRuntimeException("Router IPv4 IP provided is of incorrect format"); + } + if (isNotBlank(startIp) && isNotBlank(endIp)) { + if (!NetUtils.isIpInRange(routerIp, startIp, endIp)) { + throw new CloudRuntimeException("Router IPv4 IP provided is not within the specified range: " + startIp + " - " + endIp); + } + } else { + String cidr = NetUtils.ipAndNetMaskToCidr(gateway, netmask); + if (!NetUtils.isIpWithInCidrRange(routerIp, cidr)) { + throw new CloudRuntimeException("Router IP provided in not within the network range"); + } + } + } + if (isNotBlank(routerIpv6)) { + if (startIpv6 != null && endIpv6 == null) { + endIpv6 = startIpv6; + } + if (!NetUtils.isValidIp6(routerIpv6)) { + throw new CloudRuntimeException("Router IPv6 address provided is of incorrect format"); + } + if (isNotBlank(startIpv6) && isNotBlank(endIpv6)) { + String ipv6Range = startIpv6 + "-" + endIpv6; + if (!NetUtils.isIp6InRange(routerIpv6, ipv6Range)) { + throw new CloudRuntimeException("Router IPv6 address provided is not within the specified range: " + startIpv6 + " - " + endIpv6); + } + } else { + if (!NetUtils.isIp6InNetwork(routerIpv6, ip6Cidr)) { + throw new CloudRuntimeException("Router IPv6 address provided is not with the network range"); + } + } + } + } + @Override @DB @ActionEvent(eventType = EventTypes.EVENT_NETWORK_CREATE, eventDescription = "creating network") @@ -1042,10 +1082,14 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C String vlanId = null; boolean bypassVlanOverlapCheck = false; boolean hideIpAddressUsage = false; + String routerIp = null; + String routerIpv6 = null; if (cmd instanceof CreateNetworkCmdByAdmin) { vlanId = ((CreateNetworkCmdByAdmin)cmd).getVlan(); bypassVlanOverlapCheck = ((CreateNetworkCmdByAdmin)cmd).getBypassVlanOverlapCheck(); hideIpAddressUsage = ((CreateNetworkCmdByAdmin)cmd).getHideIpAddressUsage(); + routerIp = ((CreateNetworkCmdByAdmin)cmd).getRouterIp(); + routerIpv6 = ((CreateNetworkCmdByAdmin)cmd).getRouterIpv6(); } String name = cmd.getNetworkName(); @@ -1150,6 +1194,15 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C throw new InvalidParameterValueException("Only Admins can create network with guest type " + GuestType.Shared); } + if (ntwkOff.getGuestType() != GuestType.Shared && (isNotBlank(routerIp) || isNotBlank(routerIpv6))) { + throw new InvalidParameterValueException("Router IP can be specified only for Shared networks"); + } + + if (ntwkOff.getGuestType() == GuestType.Shared && !_networkModel.isProviderForNetworkOffering(Provider.VirtualRouter, networkOfferingId) + && (isNotBlank(routerIp) || isNotBlank(routerIpv6))) { + throw new InvalidParameterValueException("Virtual Router is not a supported provider for the Shared network, hence router ip should not be provided"); + } + // Check if the network is domain specific if (aclType == ACLType.Domain) { // only Admin can create domain with aclType=Domain @@ -1279,6 +1332,8 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C } } + validateRouterIps(routerIp, routerIpv6, startIP, endIP, gateway, netmask, startIPv6, endIPv6, ip6Cidr); + if (isNotBlank(isolatedPvlan) && (zone.getNetworkType() != NetworkType.Advanced || ntwkOff.getGuestType() == GuestType.Isolated)) { throw new InvalidParameterValueException("Can only support create Private VLAN network with advanced shared or L2 network!"); } @@ -1365,7 +1420,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C Network network = commitNetwork(networkOfferingId, gateway, startIP, endIP, netmask, networkDomain, vlanId, bypassVlanOverlapCheck, name, displayText, caller, physicalNetworkId, zoneId, domainId, isDomainSpecific, subdomainAccess, vpcId, startIPv6, endIPv6, ip6Gateway, ip6Cidr, displayNetwork, aclId, secondaryVlanId, privateVlanType, ntwkOff, pNtwk, aclType, owner, cidr, createVlan, - externalId); + externalId, routerIp, routerIpv6); if (hideIpAddressUsage) { _networkDetailsDao.persist(new NetworkDetailVO(network.getId(), Network.hideIpAddressUsage, String.valueOf(hideIpAddressUsage), false)); @@ -1445,7 +1500,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C final Boolean bypassVlanOverlapCheck, final String name, final String displayText, final Account caller, final Long physicalNetworkId, final Long zoneId, final Long domainId, final boolean isDomainSpecific, final Boolean subdomainAccessFinal, final Long vpcId, final String startIPv6, final String endIPv6, final String ip6Gateway, final String ip6Cidr, final Boolean displayNetwork, final Long aclId, final String isolatedPvlan, final PVlanType isolatedPvlanType, final NetworkOfferingVO ntwkOff, final PhysicalNetwork pNtwk, final ACLType aclType, final Account ownerFinal, - final String cidr, final boolean createVlan, final String externalId) throws InsufficientCapacityException, ResourceAllocationException { + final String cidr, final boolean createVlan, final String externalId, String routerIp, String routerIpv6) throws InsufficientCapacityException, ResourceAllocationException { try { Network network = Transaction.execute(new TransactionCallbackWithException() { @Override @@ -1500,7 +1555,7 @@ public class NetworkServiceImpl extends ManagerBase implements NetworkService, C } network = _networkMgr.createGuestNetwork(networkOfferingId, name, displayText, gateway, cidr, vlanId, bypassVlanOverlapCheck, networkDomain, owner, sharedDomainId, pNtwk, - zoneId, aclType, subdomainAccess, vpcId, ip6Gateway, ip6Cidr, displayNetwork, isolatedPvlan, isolatedPvlanType, externalId); + zoneId, aclType, subdomainAccess, vpcId, ip6Gateway, ip6Cidr, displayNetwork, isolatedPvlan, isolatedPvlanType, externalId, routerIp, routerIpv6); } if (_accountMgr.isRootAdmin(caller.getId()) && createVlan && network != null) { diff --git a/server/src/main/java/com/cloud/network/guru/DirectNetworkGuru.java b/server/src/main/java/com/cloud/network/guru/DirectNetworkGuru.java index 22811dadf43..07a563591d9 100644 --- a/server/src/main/java/com/cloud/network/guru/DirectNetworkGuru.java +++ b/server/src/main/java/com/cloud/network/guru/DirectNetworkGuru.java @@ -205,6 +205,13 @@ public class DirectNetworkGuru extends AdapterBase implements NetworkGuru { if (userSpecified.getPvlanType() != null) { config.setPvlanType(userSpecified.getPvlanType()); } + + if (userSpecified.getRouterIp() != null) { + config.setRouterIp(userSpecified.getRouterIp()); + } + if (userSpecified.getRouterIpv6() != null) { + config.setRouterIpv6(userSpecified.getRouterIpv6()); + } } boolean isSecurityGroupEnabled = _networkModel.areServicesSupportedByNetworkOffering(offering.getId(), Service.SecurityGroup); diff --git a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java index 9751ddb7667..11f03c5eab3 100644 --- a/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java +++ b/server/src/main/java/com/cloud/network/router/NetworkHelperImpl.java @@ -27,6 +27,7 @@ import java.util.Map; import javax.annotation.PostConstruct; import javax.inject.Inject; +import org.apache.cloudstack.api.ApiConstants; import org.apache.log4j.Logger; import org.cloud.network.router.deployment.RouterDeploymentDefinition; @@ -71,6 +72,8 @@ import com.cloud.network.Networks.IsolationType; import com.cloud.network.addr.PublicIp; import com.cloud.network.dao.IPAddressDao; import com.cloud.network.dao.NetworkDao; +import com.cloud.network.dao.NetworkDetailVO; +import com.cloud.network.dao.NetworkDetailsDao; import com.cloud.network.dao.UserIpv6AddressDao; import com.cloud.network.lb.LoadBalancingRule; import com.cloud.network.router.VirtualRouter.RedundantState; @@ -156,6 +159,8 @@ public class NetworkHelperImpl implements NetworkHelper { ConfigurationDao _configDao; @Inject VpcVirtualNetworkApplianceManager _vpcRouterMgr; + @Inject + NetworkDetailsDao networkDetailsDao; protected final Map> hypervisorsMap = new HashMap<>(); @@ -718,13 +723,19 @@ public class NetworkHelperImpl implements NetworkHelper { + guestNetwork); defaultNetworkStartIp = placeholder.getIPv4Address(); } else { - final String startIp = _networkModel.getStartIpAddress(guestNetwork.getId()); - if (startIp != null - && _ipAddressDao.findByIpAndSourceNetworkId(guestNetwork.getId(), startIp).getAllocatedTime() == null) { - defaultNetworkStartIp = startIp; - } else if (s_logger.isDebugEnabled()) { - s_logger.debug("First ipv4 " + startIp + " in network id=" + guestNetwork.getId() - + " is already allocated, can't use it for domain router; will get random ip address from the range"); + NetworkDetailVO routerIpDetail = networkDetailsDao.findDetail(guestNetwork.getId(), ApiConstants.ROUTER_IP); + String routerIp = routerIpDetail != null ? routerIpDetail.getValue() : null; + if (routerIp != null) { + defaultNetworkStartIp = routerIp; + } else { + final String startIp = _networkModel.getStartIpAddress(guestNetwork.getId()); + if (startIp != null + && _ipAddressDao.findByIpAndSourceNetworkId(guestNetwork.getId(), startIp).getAllocatedTime() == null) { + defaultNetworkStartIp = startIp; + } else if (s_logger.isDebugEnabled()) { + s_logger.debug("First ipv4 " + startIp + " in network id=" + guestNetwork.getId() + + " is already allocated, can't use it for domain router; will get random ip address from the range"); + } } } } @@ -735,12 +746,18 @@ public class NetworkHelperImpl implements NetworkHelper { + guestNetwork); defaultNetworkStartIpv6 = placeholder.getIPv6Address(); } else { - final String startIpv6 = _networkModel.getStartIpv6Address(guestNetwork.getId()); - if (startIpv6 != null && _ipv6Dao.findByNetworkIdAndIp(guestNetwork.getId(), startIpv6) == null) { - defaultNetworkStartIpv6 = startIpv6; - } else if (s_logger.isDebugEnabled()) { - s_logger.debug("First ipv6 " + startIpv6 + " in network id=" + guestNetwork.getId() - + " is already allocated, can't use it for domain router; will get random ipv6 address from the range"); + NetworkDetailVO routerIpDetail = networkDetailsDao.findDetail(guestNetwork.getId(), ApiConstants.ROUTER_IPV6); + String routerIpv6 = routerIpDetail != null ? routerIpDetail.getValue() : null; + if (routerIpv6 != null) { + defaultNetworkStartIpv6 = routerIpv6; + } else { + final String startIpv6 = _networkModel.getStartIpv6Address(guestNetwork.getId()); + if (startIpv6 != null && _ipv6Dao.findByNetworkIdAndIp(guestNetwork.getId(), startIpv6) == null) { + defaultNetworkStartIpv6 = startIpv6; + } else if (s_logger.isDebugEnabled()) { + s_logger.debug("First ipv6 " + startIpv6 + " in network id=" + guestNetwork.getId() + + " is already allocated, can't use it for domain router; will get random ipv6 address from the range"); + } } } } 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 15172443abe..a03e3deb103 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -2596,7 +2596,7 @@ public class VpcManagerImpl extends ManagerBase implements VpcManager, VpcProvis // 2) Create network final Network guestNetwork = _ntwkMgr.createGuestNetwork(ntwkOffId, name, displayText, gateway, cidr, vlanId, false, networkDomain, owner, domainId, pNtwk, zoneId, aclType, - subdomainAccess, vpcId, null, null, isDisplayNetworkEnabled, null, null, externalId); + subdomainAccess, vpcId, null, null, isDisplayNetworkEnabled, null, null, externalId, null, null); if (guestNetwork != null) { guestNetwork.setNetworkACLId(aclId); diff --git a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java index d4d2cd23b6b..8a363c296b7 100644 --- a/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java +++ b/server/src/main/java/com/cloud/vm/UserVmManagerImpl.java @@ -3488,7 +3488,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir s_logger.debug("Creating network for account " + owner + " from the network offering id=" + requiredOfferings.get(0).getId() + " as a part of deployVM process"); Network newNetwork = _networkMgr.createGuestNetwork(requiredOfferings.get(0).getId(), owner.getAccountName() + "-network", owner.getAccountName() + "-network", null, null, null, false, null, owner, null, physicalNetwork, zone.getId(), ACLType.Account, null, null, null, null, true, null, null, - null); + null, null, null); if (newNetwork != null) { defaultNetwork = _networkDao.findById(newNetwork.getId()); } @@ -6775,7 +6775,7 @@ public class UserVmManagerImpl extends ManagerBase implements UserVmManager, Vir Network newNetwork = _networkMgr.createGuestNetwork(requiredOfferings.get(0).getId(), newAccount.getAccountName() + "-network", newAccount.getAccountName() + "-network", null, null, null, false, null, newAccount, null, physicalNetwork, zone.getId(), ACLType.Account, null, null, - null, null, true, null, null, null); + null, null, true, null, null, null, null, null); // if the network offering has persistent set to true, implement the network if (requiredOfferings.get(0).isPersistent()) { DeployDestination dest = new DeployDestination(zone, null, null, null); diff --git a/server/src/test/java/com/cloud/network/CreatePrivateNetworkTest.java b/server/src/test/java/com/cloud/network/CreatePrivateNetworkTest.java index 72685c22321..04e3e634f09 100644 --- a/server/src/test/java/com/cloud/network/CreatePrivateNetworkTest.java +++ b/server/src/test/java/com/cloud/network/CreatePrivateNetworkTest.java @@ -123,7 +123,7 @@ public class CreatePrivateNetworkTest { ACLType.Account, false, 1L, false); when(networkService._networkMgr.createGuestNetwork(eq(ntwkOff.getId()), eq("bla"), eq("fake"), eq("10.1.1.1"), eq("10.1.1.0/24"), nullable(String.class), nullable(Boolean.class), nullable(String.class), eq(account), nullable(Long.class), eq(physicalNetwork), eq(physicalNetwork.getDataCenterId()), eq(ACLType.Account), nullable(Boolean.class), eq(1L), nullable(String.class), nullable(String.class), - nullable(Boolean.class), nullable(String.class), nullable(Network.PVlanType.class), nullable(String.class))).thenReturn(net); + nullable(Boolean.class), nullable(String.class), nullable(Network.PVlanType.class), nullable(String.class), nullable(String.class), nullable(String.class))).thenReturn(net); when( networkService._networkMgr.createPrivateNetwork(eq(ntwkOff.getId()), eq("bla"), eq("fake"), eq("10.1.1.1"), eq("10.1.1.0/24"), anyString(), anyBoolean(), eq(account), eq(physicalNetwork), eq(1L))).thenReturn(net); diff --git a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java index 1b2b0b50234..728effa8b76 100644 --- a/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java +++ b/server/src/test/java/com/cloud/vpc/MockNetworkManagerImpl.java @@ -646,7 +646,7 @@ public class MockNetworkManagerImpl extends ManagerBase implements NetworkOrches @Override public Network createGuestNetwork(long networkOfferingId, String name, String displayText, String gateway, String cidr, String vlanId, boolean bypassVlanOverlapCheck, String networkDomain, Account owner, Long domainId, PhysicalNetwork physicalNetwork, long zoneId, ACLType aclType, Boolean subdomainAccess, Long vpcId, String gatewayv6, - String cidrv6, Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId) throws ConcurrentOperationException, InsufficientCapacityException, + String cidrv6, Boolean displayNetworkEnabled, String isolatedPvlan, Network.PVlanType isolatedPvlanType, String externalId, String routerIp, String routerIpv6) throws ConcurrentOperationException, InsufficientCapacityException, ResourceAllocationException { // TODO Auto-generated method stub return null; diff --git a/test/integration/smoke/test_network.py b/test/integration/smoke/test_network.py index 99d96d7bdef..70b4bbc796a 100644 --- a/test/integration/smoke/test_network.py +++ b/test/integration/smoke/test_network.py @@ -23,7 +23,7 @@ from marvin.cloudstackTestCase import cloudstackTestCase from marvin.cloudstackException import CloudstackAPIException from marvin.cloudstackAPI import rebootRouter from marvin.sshClient import SshClient -from marvin.lib.utils import cleanup_resources, get_process_status +from marvin.lib.utils import cleanup_resources, get_process_status, get_host_credentials from marvin.lib.base import (Account, VirtualMachine, ServiceOffering, @@ -38,7 +38,9 @@ from marvin.lib.base import (Account, NIC, Cluster) from marvin.lib.common import (get_domain, + get_free_vlan, get_zone, + get_template, get_test_template, list_hosts, list_publicIP, @@ -54,6 +56,7 @@ from ddt import ddt, data # Import System modules import time import logging +import random _multiprocess_shared_ = True @@ -1838,3 +1841,195 @@ class TestPrivateVlansL2Networks(cloudstackTestCase): self.fail("Failing test. Error: %s" % e) return + + +class TestSharedNetwork(cloudstackTestCase): + + @classmethod + def setUpClass(cls): + cls.testClient = super(TestSharedNetwork, cls).getClsTestClient() + cls.apiclient = cls.testClient.getApiClient() + + cls.services = cls.testClient.getParsedTestDataConfig() + # Get Zone, Domain and templates + cls.domain = get_domain(cls.apiclient) + cls.zone = get_zone(cls.apiclient, cls.testClient.getZoneForTests()) + cls.template = get_template(cls.apiclient, cls.zone.id, + cls.services["ostype"]) + + cls.services["virtual_machine"]["zoneid"] = cls.zone.id + cls.services["virtual_machine"]["template"] = cls.template.id + # Create Network Offering + cls.services["shared_network_offering"]["specifyVlan"] = "True" + cls.services["shared_network_offering"]["specifyIpRanges"] = "True" + cls.shared_network_offering = NetworkOffering.create(cls.apiclient, cls.services["shared_network_offering"], + conservemode=False) + + # Update network offering state from disabled to enabled. + NetworkOffering.update(cls.shared_network_offering, cls.apiclient, state="enabled") + + cls.service_offering = ServiceOffering.create(cls.apiclient, cls.services["service_offering"]) + physical_network, vlan = get_free_vlan(cls.apiclient, cls.zone.id) + # create network using the shared network offering created + + cls.services["shared_network"]["acltype"] = "domain" + cls.services["shared_network"]["vlan"] = vlan + cls.services["shared_network"]["networkofferingid"] = cls.shared_network_offering.id + cls.services["shared_network"]["physicalnetworkid"] = physical_network.id + + cls.setSharedNetworkParams("shared_network") + cls.shared_network = Network.create(cls.apiclient, + cls.services["shared_network"], + networkofferingid=cls.shared_network_offering.id, + zoneid=cls.zone.id) + cls._cleanup = [ + cls.service_offering, + cls.shared_network_offering + ] + return + + def setUp(self): + self.apiclient = self.testClient.getApiClient() + self.dbclient = self.testClient.getDbConnection() + self.cleanup = [] + + @classmethod + def tearDownClass(cls): + try: + # Cleanup resources used + cleanup_resources(cls.apiclient, cls._cleanup) + except Exception as e: + raise Exception("Warning: Exception during cleanup : %s" % e) + return + + def tearDown(self): + cleanup_resources(self.apiclient, self.cleanup) + return + + @classmethod + def setSharedNetworkParams(cls, network, range=20): + + # @range: range decides the endip. Pass the range as "x" if you want the difference between the startip + # and endip as "x" + # Set the subnet number of shared networks randomly prior to execution + # of each test case to avoid overlapping of ip addresses + shared_network_subnet_number = random.randrange(1, 254) + cls.services[network]["routerip"] = "172.16." + str(shared_network_subnet_number) + "." + str(15) + cls.services[network]["gateway"] = "172.16." + str(shared_network_subnet_number) + ".1" + cls.services[network]["startip"] = "172.16." + str(shared_network_subnet_number) + ".2" + cls.services[network]["endip"] = "172.16." + str(shared_network_subnet_number) + "." + str(range + 1) + cls.services[network]["netmask"] = "255.255.255.0" + logger.debug("Executing command '%s'" % cls.services[network]) + + def get_router_host(self, router): + self.assertEqual( + router.state, + 'Running', + "Check list router response for router state" + ) + hosts = list_hosts( + self.apiclient, + id=router.hostid) + self.assertEqual( + isinstance(hosts, list), + True, + "Check for list hosts response return valid data") + host = hosts[0] + if host.hypervisor.lower() in ("vmware", "hyperv"): + host.ipaddress = self.apiclient.connection.mgtSvr + host.user = self.apiclient.connection.user + host.password = self.apiclient.connection.passwd + host.port = 22 + else: + host.user, host.password = get_host_credentials(self.config, host.ipaddress) + host.port = 22 + return host + + def verify_ip_address_in_router(self, router, host, ipaddress, device, isExist=True): + command = 'ip addr show %s |grep "inet "|cut -d " " -f6 |cut -d "/" -f1 |grep -w %s' % (device, ipaddress) + logger.debug("Executing command '%s'" % command) + result = get_process_status( + host.ipaddress, + host.port, + host.user, + host.password, + router.linklocalip, + command, + host.hypervisor.lower()) + self.assertEqual(len(result) > 0 and result[0] == ipaddress, isExist, "ip %s verification failed" % ipaddress) + + @attr(tags=["advanced", "shared"]) + def test_01_deployVMInSharedNetwork(self): + try: + self.virtual_machine = VirtualMachine.create(self.apiclient, self.services["virtual_machine"], + networkids=[self.shared_network.id], + serviceofferingid=self.service_offering.id + ) + except Exception as e: + self.fail("Exception while deploying virtual machine: %s" % e) + + routerIp = self.services["shared_network"]["routerip"] + nic_ip_address = self.dbclient.execute( + "select ip4_address from nics where strategy='Placeholder' and ip4_address = '%s';" % routerIp); + + self.assertNotEqual( + len(nic_ip_address), + 0, + "Placeholder ip for the VR in shared network isn't the same as what was passed" + ) + + routers = Router.list( + self.apiclient, + networkid=self.shared_network.id, + listall=True + ) + + for router in routers: + host = self.get_router_host(router) + self.verify_ip_address_in_router(router, host, routerIp, "eth0", True) + + # expunge VM + VirtualMachine.delete(self.virtual_machine, self.apiclient, expunge=True) + + @attr(tags=["advanced", "shared"]) + def test_02_verifyRouterIpAfterNetworkRestart(self): + routerIp = self.services["shared_network"]["routerip"] + self.debug("restarting network with cleanup") + try: + self.shared_network.restart(self.apiclient, cleanup=True) + except Exception as e: + self.fail("Failed to cleanup network - %s" % e) + + self.debug("Listing routers for network: %s" % self.shared_network.name) + routers = Router.list( + self.apiclient, + networkid=self.shared_network.id, + listall=True + ) + self.assertEqual( + len(routers), + 1, + "Router for the shared network wasn't found)" + ) + + for router in routers: + host = self.get_router_host(router) + self.verify_ip_address_in_router(router, host, routerIp, "eth0", True) + + @attr(tags=["advanced", "shared"]) + def test_03_destroySharedNetwork(self): + routerIp = self.services["shared_network"]["routerip"] + try: + self.shared_network.delete(self.apiclient) + except Exception as e: + self.fail("Failed to destroy the shared network") + + self.debug("Fetch the placeholder record for the router") + nic_ip_address = self.dbclient.execute( + "select ip4_address from nics where strategy='Placeholder' and ip4_address = '%s' and removed is NOT NULL;" % routerIp); + + self.assertNotEqual( + len(nic_ip_address), + 0, + "Failed to find the placeholder IP" + ) \ No newline at end of file diff --git a/tools/marvin/marvin/lib/base.py b/tools/marvin/marvin/lib/base.py index 6bb7b669096..20e116ae1bc 100755 --- a/tools/marvin/marvin/lib/base.py +++ b/tools/marvin/marvin/lib/base.py @@ -3158,6 +3158,18 @@ class Network: cmd.isolatedpvlan = services["isolatedpvlan"] if "isolatedpvlantype" in services: cmd.isolatedpvlantype = services["isolatedpvlantype"] + if "routerip" in services: + cmd.routerip = services["routerip"] + if "ip6gateway" in services: + cmd.ip6gateway = services["ip6gateway"] + if "ip6cidr" in services: + cmd.ip6cidr = services["ip6cidr"] + if "startipv6" in services: + cmd.startipv6 = services["startipv6"] + if "endipv6" in services: + cmd.endipv6 = services["endipv6"] + if "routeripv6" in services: + cmd.routeripv6 = services["routeripv6"] if accountid: cmd.account = accountid diff --git a/ui/public/locales/en.json b/ui/public/locales/en.json index f14878c1115..3241c82309d 100644 --- a/ui/public/locales/en.json +++ b/ui/public/locales/en.json @@ -1815,6 +1815,8 @@ "label.root.disk.size": "Root disk size (GB)", "label.rootdiskcontrollertype": "Root disk controller", "label.rootdiskcontrollertypekvm": "Root disk controller", +"label.routerip": "IPv4 address for Router in Shared Network", +"label.routeripv6": "IPv6 address for Router in Shared Network", "label.router.health.check.last.updated": "Last updated", "label.router.health.check.name": "Check name", "label.router.health.check.success": "Success", diff --git a/ui/src/views/network/CreateSharedNetworkForm.vue b/ui/src/views/network/CreateSharedNetworkForm.vue index e91a5631c4a..7e9ddd8973c 100644 --- a/ui/src/views/network/CreateSharedNetworkForm.vue +++ b/ui/src/views/network/CreateSharedNetworkForm.vue @@ -332,6 +332,17 @@ v-decorator="['endipv4', {}]" :placeholder="this.$t('label.endipv4')"/> + + + {{ $t('label.routerip') }} + + + + + + {{ $t('label.ip6gateway') }} @@ -376,6 +387,17 @@ v-decorator="['endipv6', {}]" :placeholder="this.$t('label.endipv6')"/> + + + {{ $t('label.routeripv6') }} + + + + + + {{ $t('label.networkdomain') }} @@ -457,7 +479,9 @@ export default { selectedNetworkOffering: {}, projects: [], projectLoading: false, - selectedProject: {} + selectedProject: {}, + isVirtualRouterForAtLeastOneService: false, + selectedServiceProviderMap: {} } }, watch: { @@ -654,6 +678,7 @@ export default { this.networkOfferings = [] api('listNetworkOfferings', params).then(json => { this.networkOfferings = json.listnetworkofferingsresponse.networkoffering + this.handleNetworkOfferingChange(this.networkOfferings[0]) }).catch(error => { this.$notifyError(error) }).finally(() => { @@ -662,7 +687,6 @@ export default { this.form.setFieldsValue({ networkofferingid: 0 }) - this.handleNetworkOfferingChange(this.networkOfferings[0]) } else { this.form.setFieldsValue({ networkofferingid: null @@ -672,6 +696,27 @@ export default { }, handleNetworkOfferingChange (networkOffering) { this.selectedNetworkOffering = networkOffering + if (networkOffering) { + this.networkServiceProviderMap(this.selectedNetworkOffering.id) + } + }, + networkServiceProviderMap (id) { + api('listNetworkOfferings', { id: id }).then(json => { + var networkOffering = json.listnetworkofferingsresponse.networkoffering[0] + const services = networkOffering.service + this.selectedServiceProviderMap = {} + for (const svc of services) { + this.selectedServiceProviderMap[svc.name] = svc.provider[0].name + } + var providers = Object.values(this.selectedServiceProviderMap) + this.isVirtualRouterForAtLeastOneService = false + var self = this + providers.forEach(function (prvdr, idx) { + if (prvdr === 'VirtualRouter') { + self.isVirtualRouterForAtLeastOneService = true + } + }) + }) }, fetchDomainData () { const params = {} @@ -791,6 +836,9 @@ export default { if (this.isValidTextValueForKey(values, 'ip4gateway')) { params.ip6gateway = values.ip6gateway } + if (this.isValidTextValueForKey(values, 'routerip')) { + params.routerip = values.routerip + } if (this.isValidTextValueForKey(values, 'ip6cidr')) { params.ip6cidr = values.ip6cidr } @@ -800,6 +848,9 @@ export default { if (this.isValidTextValueForKey(values, 'endipv6')) { params.endipv6 = values.endipv6 } + if (this.isValidTextValueForKey(values, 'routeripv6')) { + params.routeripv6 = values.routeripv6 + } // IPv6 (end) if (this.isValidTextValueForKey(values, 'networkdomain')) {