diff --git a/api/src/main/java/com/cloud/network/netris/NetrisService.java b/api/src/main/java/com/cloud/network/netris/NetrisService.java index 110e9f07105a..1e7c7d66399a 100644 --- a/api/src/main/java/com/cloud/network/netris/NetrisService.java +++ b/api/src/main/java/com/cloud/network/netris/NetrisService.java @@ -21,6 +21,8 @@ import com.cloud.network.SDNProviderNetworkRule; import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.utils.net.NetUtils; import java.util.List; @@ -81,18 +83,20 @@ public interface NetrisService { /** * Creates a virtual network (vNet) resource. * - * @param zoneId the ID of the zone - * @param accountId the ID of the account - * @param domainId the ID of the domain - * @param vpcName the name of the VPC - * @param vpcId the ID of the VPC - * @param networkName the name of the network - * @param networkId the ID of the network - * @param cidr the CIDR of the network - * @param globalRouting true if global routing is enabled + * @param zoneId the ID of the zone + * @param accountId the ID of the account + * @param domainId the ID of the domain + * @param vpcName the name of the VPC + * @param vpcId the ID of the VPC + * @param networkName the name of the network + * @param networkId the ID of the network + * @param cidr the CIDR of the network + * @param networkMode the network mode + * @param internetProtocol the internet protocol + * @param isL2 true if it is an L2 network * @return true if the operation is successful, false otherwise */ - boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting); + boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2); /** * Updates an existing vNet resource. @@ -105,9 +109,10 @@ public interface NetrisService { * @param networkName the new name of the network * @param networkId the ID of the network * @param prevNetworkName the previous name of the network + * @param isL2 true if it is an L2 network * @return true if the operation is successful, false otherwise */ - boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName); + boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2); /** * Deletes an existing vNet resource. @@ -120,9 +125,10 @@ public interface NetrisService { * @param networkName the name of the network * @param networkId the ID of the network * @param cidr the CIDR of the network + * @param isL2 true if it is an L2 network * @return true if the operation is successful, false otherwise */ - boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr); + boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2); /** * Creates a source NAT rule for a VPC or network. 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 a8ff00c40ff3..05d26278101d 100644 --- a/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java +++ b/api/src/main/java/org/apache/cloudstack/api/ApiConstants.java @@ -1288,6 +1288,7 @@ public class ApiConstants { public static final String HAS_RULES = "hasrules"; public static final String NSX_DETAIL_KEY = "forNsx"; public static final String NETRIS_DETAIL_KEY = "forNetris"; + public static final String NETRIS_NAT_PUBLIC_IP_ID = "netrisnatpublicipid"; public static final String NETRIS_TAG = "netristag"; public static final String NETRIS_VXLAN_ID = "netrisvxlanid"; public static final String NETRIS_URL = "netrisurl"; diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java index a0559f57dab0..da71b6596c09 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/network/CreateNetworkOfferingCmd.java @@ -60,6 +60,7 @@ import static com.cloud.network.Network.Service.NetworkACL; import static com.cloud.network.Network.Service.UserData; import static com.cloud.network.Network.Service.Firewall; +import static com.cloud.network.Network.Service.Vpn; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; @@ -309,6 +310,10 @@ public List getSupportedServices() { } else { services.add(Firewall.getName()); } + + if (isNetrisNatted(getProvider(), getNetworkMode())) { + services.add(Vpn.getName()); + } return services; } } @@ -418,6 +423,9 @@ else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMo serviceProviderMap.remove(Lb.getName()); } } + if (isNetrisNatted(getProvider(), getNetworkMode())) { + serviceProviderMap.put(Vpn.getName(), List.of(routerProvider)); + } } public Map getServiceCapabilities(Service service) { diff --git a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java index 608794842ed4..0897a2b1b428 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/admin/vpc/CreateVPCOfferingCmd.java @@ -24,6 +24,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -61,7 +62,9 @@ import static com.cloud.network.Network.Service.NetworkACL; import static com.cloud.network.Network.Service.UserData; import static com.cloud.network.Network.Service.Gateway; +import static com.cloud.network.Network.Service.Vpn; +import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetris; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisNatted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNetrisRouted; import static org.apache.cloudstack.api.command.utils.OfferingUtils.isNsxWithoutLb; @@ -189,7 +192,7 @@ public List getSupportedServices() { NetworkACL.getName(), UserData.getName() )); - if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + if (isNetrisNatted(getProvider(), getNetworkMode())) { supportedServices.addAll(Arrays.asList( StaticNat.getName(), SourceNat.getName(), @@ -198,9 +201,13 @@ public List getSupportedServices() { if (NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(getNetworkMode())) { supportedServices.add(Gateway.getName()); } + if (getNsxSupportsLbService() || isNetrisNatted(getProvider(), getNetworkMode())) { supportedServices.add(Lb.getName()); } + if (isNetrisNatted(getProvider(), getNetworkMode())) { + supportedServices.add(Vpn.getName()); + } } return supportedServices; } @@ -210,7 +217,7 @@ public String getProvider() { } public String getNetworkMode() { - return networkMode; + return isNetris(getProvider()) && Objects.isNull(networkMode) ? NetworkOffering.NetworkMode.NATTED.name() : networkMode; } public boolean getNsxSupportsLbService() { @@ -248,7 +255,7 @@ public Map> getServiceProviders() { private void getServiceProviderMapForExternalProvider(Map> serviceProviderMap, String provider) { List unsupportedServices = new ArrayList<>(List.of("Vpn", "BaremetalPxeService", "SecurityGroup", "Connectivity", "Firewall")); - if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode())) { + if (isNetrisNatted(getProvider(), getNetworkMode())) { unsupportedServices.add("Gateway"); } List routerSupported = List.of("Dhcp", "Dns", "UserData"); @@ -258,7 +265,7 @@ private void getServiceProviderMapForExternalProvider(Map> continue; if (routerSupported.contains(service)) serviceProviderMap.put(service, List.of(VirtualRouterProvider.Type.VPCVirtualRouter.name())); - else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMode()) || + else if (isNetrisNatted(getProvider(), getNetworkMode()) || Stream.of(NetworkACL.getName(), Gateway.getName()).anyMatch(s -> s.equalsIgnoreCase(service))) { serviceProviderMap.put(service, List.of(provider)); } @@ -268,7 +275,20 @@ else if (NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(getNetworkMo } } + @SuppressWarnings("unchecked") public Map> getServiceCapabilityList() { + // For Netris NATTED, Gateway service is excluded so the redundant-router capability + // must reference SourceNat (not Gateway). + if (serviceCapabilityList != null + && isNetrisNatted(getProvider(), getNetworkMode())) { + for (Object capEntry : serviceCapabilityList.values()) { + Map cap = (Map) capEntry; + if (Network.Capability.RedundantRouter.getName().equalsIgnoreCase(cap.get("capabilitytype")) + && Gateway.getName().equalsIgnoreCase(cap.get("service"))) { + cap.put("service", SourceNat.getName()); + } + } + } return serviceCapabilityList; } diff --git a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java index 433a37c07cde..3885c30f0af4 100644 --- a/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java +++ b/api/src/main/java/org/apache/cloudstack/api/command/utils/OfferingUtils.java @@ -23,8 +23,12 @@ public class OfferingUtils { private OfferingUtils() { } + public static boolean isNetris(String provider) { + return "Netris".equalsIgnoreCase(provider); + } + public static boolean isNetrisNatted(String provider, String networkMode) { - return "Netris".equalsIgnoreCase(provider) && + return isNetris(provider) && NetworkOffering.NetworkMode.NATTED.name().equalsIgnoreCase(networkMode); } @@ -33,6 +37,6 @@ public static boolean isNsxWithoutLb(String provider, boolean nsxSupportsLbServi } public static boolean isNetrisRouted(String provider, String networkMode) { - return "Netris".equalsIgnoreCase(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode); + return isNetris(provider) && NetworkOffering.NetworkMode.ROUTED.name().equalsIgnoreCase(networkMode); } } diff --git a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java index a0516e660e48..6c26a45b4826 100644 --- a/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java +++ b/api/src/main/java/org/apache/cloudstack/api/response/VpcOfferingResponse.java @@ -62,6 +62,10 @@ public class VpcOfferingResponse extends BaseResponse { @Param(description = "Indicates if the VPC offering supports distributed router for one-hop forwarding", since = "4.4") private Boolean supportsDistributedRouter; + @SerializedName(ApiConstants.REDUNDANT_ROUTER) + @Param(description = "Indicates if the VPC offering supports redundant routers") + private Boolean redundantRouter; + @SerializedName((ApiConstants.SUPPORTS_REGION_LEVEL_VPC)) @Param(description = "Indicated if the offering can support region level VPC", since = "4.4") private Boolean supportsRegionLevelVpc; @@ -201,4 +205,8 @@ public String getRoutingMode() { public void setRoutingMode(String routingMode) { this.routingMode = routingMode; } + + public void setRedundantRouter(Boolean redundantRouter) { + this.redundantRouter = redundantRouter; + } } 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 55079173dc98..c082cffb1fb2 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 @@ -2302,6 +2302,11 @@ public void prepareNicForMigration(final VirtualMachineProfile vm, final DeployD } } + private boolean isReservedForExternalProvider(Long vlanId) { + VlanDetailsVO vlanDetail = vlanDetailsDao.findDetail(vlanId, ApiConstants.NETRIS_DETAIL_KEY); + return vlanDetail != null && BooleanUtils.toBoolean(vlanDetail.getValue()); + } + /* Prepare All Nics for migration including the nics dynamically created and not stored in DB This is a temporary workaround work KVM migration @@ -2350,6 +2355,10 @@ public void prepareAllNicsForMigration(final VirtualMachineProfile vm, final Dep final List publicIps = _ipAddressDao.listByAssociatedNetwork(guestNetworkId, null); for (final IPAddressVO userIp : publicIps) { final PublicIp publicIp = PublicIp.createFromAddrAndVlan(userIp, _vlanDao.findById(userIp.getVlanId())); + if (isReservedForExternalProvider(publicIp.getVlanId())) { + logger.debug("Skipping public IP {} for migration NIC profile: managed by external network provider", publicIp.getAddress()); + continue; + } final URI broadcastUri = BroadcastDomainType.Vlan.toUri(publicIp.getVlanTag()); final long ntwkId = publicIp.getNetworkId(); final Nic nic = _nicDao.findByNetworkIdInstanceIdAndBroadcastUri(ntwkId, vm.getId(), @@ -3173,9 +3182,10 @@ private boolean hasGuestBypassVlanOverlapCheck(final boolean bypassVlanOverlapCh } /** - * Checks for L2 network offering services. Only 2 cases allowed: + * Checks for L2 network offering services. Only 3 cases allowed: * - No services - * - User Data service only, provided by ConfigDrive + * - UserData service only, provided by ConfigDrive + * - Connectivity service only, provided by Netris * * @param ntwkOff network offering */ diff --git a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql index baa20e9f0ad5..bcbe96ef7cb3 100644 --- a/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql +++ b/engine/schema/src/main/resources/META-INF/db/schema-42200to42210.sql @@ -59,3 +59,11 @@ UPDATE `cloud`.`configuration` SET value = CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team') WHERE name = 'user.password.reset.mail.template' AND value IN (CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', 'http://{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team'), CONCAT_WS('\n', 'Hello {{username}}!', 'You have requested to reset your password. Please click the following link to reset your password:', '{{{domainUrl}}}{{{resetLink}}}', 'If you did not request a password reset, please ignore this email.', '', 'Regards,', 'The CloudStack Team')); + +-- Reclaim VNETs belonging to Netris deleted networks +UPDATE op_dc_vnet_alloc a + JOIN networks n ON n.broadcast_uri = CONCAT('netris://', a.vnet) + SET a.account_id = NULL, + a.reservation_id = NULL, + a.taken = NULL +WHERE n.removed IS NOT NULL; \ No newline at end of file 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 d19470f8bab2..9b9ed32e7ea3 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 @@ -300,6 +300,8 @@ public class KubernetesClusterManagerImpl extends ManagerBase implements Kuberne private static final String DEFAULT_NSX_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNSXVPCNetworkOfferingforKubernetesService"; private static final String DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for NSX CloudStack Kubernetes Service"; private static final String DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for NSX CloudStack Kubernetes service on VPC"; + private static final String DEFAULT_NETRIS_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME = "DefaultNetrisVPCNetworkOfferingforKubernetesService"; + private static final String DEFAULT_NETRIS_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT = "Network Offering for Netris CloudStack Kubernetes Service on VPC"; protected StateMachine2 _stateMachine = KubernetesCluster.State.getStateMachine(); @@ -598,6 +600,21 @@ private void validateIsolatedNetwork(Network network, int clusterTotalNodeCount) validateIsolatedNetworkIpRules(sourceNatIp.getId(), FirewallRule.Purpose.PortForwarding, network, clusterTotalNodeCount); } + public boolean isNetrisNetwork(Network network) { + return networkModel.isProviderSupportServiceInNetwork( + network.getId(), Service.Lb, Network.Provider.Netris); + } + + protected void validateNetrisNetwork(Network network) { + if (network.getVpcId() == null) { + throw new InvalidParameterValueException(String.format( + "Netris Kubernetes provider supports only VPC networks / tiers. " + + "Network '%s' (id: %s) is not part of a VPC. " + + "Please create the cluster within a VPC tier.", + network.getName(), network.getUuid())); + } + } + protected void validateVpcTier(Network network) { if (Network.State.Allocated.equals(network.getState())) { // Allocated networks won't have IP and rules return; @@ -1146,6 +1163,9 @@ private Network getKubernetesClusterNetworkIfMissing(final String clusterName, f Network network = null; if (networkId != null) { network = networkDao.findById(networkId); + if (isNetrisNetwork(network)) { + validateNetrisNetwork(network); + } if (Network.GuestType.Isolated.equals(network.getGuestType())) { if (kubernetesClusterDao.listByNetworkId(network.getId()).isEmpty()) { validateNetwork(network, controlNodesCount + nodesCount); @@ -2689,13 +2709,16 @@ private boolean isClusterVMsInDesiredState(KubernetesCluster kubernetesCluster, @Override public boolean start() { createNetworkOfferingForKubernetes(DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME, - DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, false, false); + DEFAULT_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, null, false); createNetworkOfferingForKubernetes(DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME, - DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, true, false); + DEFAULT_NSX_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT, Network.Provider.Nsx, false); createNetworkOfferingForKubernetes(DEFAULT_NSX_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME, - DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , true, true); + DEFAULT_NSX_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , Network.Provider.Nsx, true); + + createNetworkOfferingForKubernetes(DEFAULT_NETRIS_VPC_TIER_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_NAME, + DEFAULT_NETRIS_VPC_NETWORK_OFFERING_FOR_KUBERNETES_SERVICE_DISPLAY_TEXT , Network.Provider.Netris, true); getProjectKubernetesAccountRole(); @@ -2705,23 +2728,23 @@ public boolean start() { return true; } - private void createNetworkOfferingForKubernetes(String offeringName, String offeringDesc, boolean forNsx, boolean forVpc) { + private void createNetworkOfferingForKubernetes(String offeringName, String offeringDesc, Network.Provider extNetProvider, boolean forVpc) { final Map defaultKubernetesServiceNetworkOfferingProviders = new HashMap(); Network.Provider provider = forVpc ? Network.Provider.VPCVirtualRouter : Network.Provider.VirtualRouter; defaultKubernetesServiceNetworkOfferingProviders.put(Service.Dhcp, provider); defaultKubernetesServiceNetworkOfferingProviders.put(Service.Dns, provider); defaultKubernetesServiceNetworkOfferingProviders.put(Service.UserData, provider); if (forVpc) { - defaultKubernetesServiceNetworkOfferingProviders.put(Service.NetworkACL, forNsx ? Network.Provider.Nsx : provider); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.NetworkACL, Objects.requireNonNullElse(extNetProvider, provider)); } else { - defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, forNsx ? Network.Provider.Nsx : provider); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.Firewall, Objects.requireNonNullElse(extNetProvider, provider)); } - defaultKubernetesServiceNetworkOfferingProviders.put(Service.Lb, forNsx ? Network.Provider.Nsx : provider); - defaultKubernetesServiceNetworkOfferingProviders.put(Service.SourceNat, forNsx ? Network.Provider.Nsx : provider); - defaultKubernetesServiceNetworkOfferingProviders.put(Service.StaticNat, forNsx ? Network.Provider.Nsx : provider); - defaultKubernetesServiceNetworkOfferingProviders.put(Service.PortForwarding, forNsx ? Network.Provider.Nsx : provider); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.Lb, Objects.requireNonNullElse(extNetProvider, provider)); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.SourceNat, Objects.requireNonNullElse(extNetProvider, provider)); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.StaticNat, Objects.requireNonNullElse(extNetProvider, provider)); + defaultKubernetesServiceNetworkOfferingProviders.put(Service.PortForwarding, Objects.requireNonNullElse(extNetProvider, provider)); - if (!forNsx) { + if (Objects.isNull(extNetProvider)) { defaultKubernetesServiceNetworkOfferingProviders.put(Service.Gateway, Network.Provider.VirtualRouter); defaultKubernetesServiceNetworkOfferingProviders.put(Service.Vpn, Network.Provider.VirtualRouter); } @@ -2734,7 +2757,7 @@ private void createNetworkOfferingForKubernetes(String offeringName, String offe true, false, false, false, false, false, false, false, true, true, false, forVpc, true, false, false); - if (forNsx) { + if (Objects.nonNull(extNetProvider)) { defaultKubernetesServiceNetworkOffering.setNetworkMode(NetworkOffering.NetworkMode.NATTED); } defaultKubernetesServiceNetworkOffering.setSupportsVmAutoScaling(true); @@ -2746,7 +2769,7 @@ private void createNetworkOfferingForKubernetes(String offeringName, String offe new NetworkOfferingServiceMapVO(defaultKubernetesServiceNetworkOffering.getId(), service, defaultKubernetesServiceNetworkOfferingProviders.get(service)); networkOfferingServiceMapDao.persist(offService); - logger.trace("Added service for the network offering: " + offService); + logger.trace("Added service for the network offering: {}", offService); } } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java index 1d4b6e8d0a84..7a06b11e3bf4 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterActionWorker.java @@ -79,6 +79,8 @@ import org.apache.commons.lang3.StringUtils; import com.cloud.dc.DataCenterVO; +import com.cloud.dc.Vlan.VlanType; +import com.cloud.dc.VlanVO; import com.cloud.dc.dao.DataCenterDao; import com.cloud.dc.dao.VlanDao; import com.cloud.exception.InsufficientAddressCapacityException; @@ -101,6 +103,7 @@ import com.cloud.network.NetworkModel; import com.cloud.network.NetworkService; import com.cloud.network.dao.IPAddressDao; +import com.cloud.network.dao.IPAddressVO; import com.cloud.network.dao.NetworkDao; import com.cloud.network.vpc.VpcService; import com.cloud.projects.ProjectService; @@ -227,8 +230,27 @@ public class KubernetesClusterActionWorker { protected VirtualMachineTemplate etcdTemplate; protected File sshKeyFile; protected String publicIpAddress; + /** For Netris VPC-tier clusters the Kubernetes API is exposed via the L4 LB rule on the LB IP. + * For all other clusters: same value as {@link #publicIpAddress}. */ + protected String apiPublicIpAddress; protected int sshPort; + /** + * Returns the public IP that Kubernetes nodes should use as the API server cluster endpoint. + * For Netris VPC-tier clusters this is the LB IP (stored under {@code PUBLIC_IP_ID}); for all + * other clusters it falls back to {@link #publicIpAddress}. + * {@link #publicIpAddress} must be set before calling this method. + */ + protected String resolveApiPublicIpAddress(Network network) { + if (network != null && network.getVpcId() != null && manager.isNetrisNetwork(network)) { + IpAddress lbIp = getVpcTierKubernetesPublicIp(network); + if (lbIp != null) { + return lbIp.getAddress().addr(); + } + } + return publicIpAddress; + } + protected final String deploySecretsScriptFilename = "deploy-cloudstack-secret"; protected final String deployProviderScriptFilename = "deploy-provider"; @@ -439,7 +461,11 @@ protected IpAddress getNetworkSourceNatIp(Network network) { } protected IpAddress getVpcTierKubernetesPublicIp(Network network) { - KubernetesClusterDetailsVO detailsVO = kubernetesClusterDetailsDao.findDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID); + return getVpcTierKubernetesPublicIp(network, ApiConstants.PUBLIC_IP_ID); + } + + protected IpAddress getVpcTierKubernetesPublicIp(Network network, String detailKey) { + KubernetesClusterDetailsVO detailsVO = kubernetesClusterDetailsDao.findDetail(kubernetesCluster.getId(), detailKey); if (detailsVO == null || StringUtils.isEmpty(detailsVO.getValue())) { return null; } @@ -458,7 +484,8 @@ protected IpAddress getVpcTierKubernetesPublicIp(Network network) { protected IpAddress getPublicIp(Network network) throws ManagementServerException { if (network.getVpcId() != null) { - IpAddress publicIp = getVpcTierKubernetesPublicIp(network); + String detailKey = manager.isNetrisNetwork(network) ? ApiConstants.NETRIS_NAT_PUBLIC_IP_ID : ApiConstants.PUBLIC_IP_ID; + IpAddress publicIp = getVpcTierKubernetesPublicIp(network, detailKey); if (publicIp == null) { throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName())); } @@ -474,14 +501,24 @@ protected IpAddress getPublicIp(Network network) throws ManagementServerExceptio protected IpAddress acquireVpcTierKubernetesPublicIp(Network network, boolean forEtcd) throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { - IpAddress ip = networkService.allocateIP(owner, kubernetesCluster.getZoneId(), network.getId(), null, null); + return acquireVpcTierKubernetesPublicIp(network, forEtcd ? null : ApiConstants.PUBLIC_IP_ID); + } + + protected IpAddress acquireVpcTierKubernetesPublicIp(Network network, String detailKey) throws + InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { + IpAddress ip; + if (manager.isNetrisNetwork(network)) { + ip = allocateIpFromNetrisRange(network); + } else { + ip = networkService.allocateIP(owner, kubernetesCluster.getZoneId(), network.getId(), null, null); + } if (ip == null) { return null; } ip = vpcService.associateIPToVpc(ip.getId(), network.getVpcId()); ip = ipAddressManager.associateIPToGuestNetwork(ip.getId(), network.getId(), false); - if (!forEtcd) { - kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID, ip.getUuid(), false); + if (detailKey != null) { + kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), detailKey, ip.getUuid(), false); } return ip; } @@ -496,6 +533,39 @@ protected IpAddress acquirePublicIpForIsolatedNetwork(Network network) throws return ip; } + /** + * Finds and allocates a free public IP from the Netris-reserved VLAN range in the cluster's + * zone. Used for both the LB IP ({@code PUBLIC_IP_ID}) and the NAT IP + * ({@code NETRIS_NAT_PUBLIC_IP_ID}): all IPs acquired for a Netris cluster must come from + * VLANs tagged {@code forNetris=true} in {@code vlan_details}. The actual allocation is + * delegated to {@link NetworkService#allocateIP} with the discovered address so that the + * normal account-lock / state-transition path is preserved. + * + * @return the allocated {@link IpAddress}, or {@code null} if the Netris range is exhausted + */ + private IpAddress allocateIpFromNetrisRange(Network network) throws + InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { + long dcId = kubernetesCluster.getZoneId(); + List netrisVlanIds = vlanDao.listVlansForExternalNetworkProvider(dcId, ApiConstants.NETRIS_DETAIL_KEY) + .stream().map(VlanVO::getId).collect(Collectors.toList()); + if (netrisVlanIds.isEmpty()) { + throw new CloudRuntimeException(String.format( + "Cannot allocate Netris LB IP for Kubernetes cluster %s: no VLANs tagged " + + "'forNetris=true' found in zone %d", kubernetesCluster.getName(), dcId)); + } + List available = ipAddressManager.listAvailablePublicIps( + dcId, null, netrisVlanIds, owner, VlanType.VirtualNetwork, + network.getId(), false, false, false, null, null, false, + null, null, false, false); + if (available.isEmpty()) { + logger.warn(String.format("No available IPs in the Netris range for zone %d, " + + "Kubernetes cluster %s", dcId, kubernetesCluster.getName())); + return null; + } + String requestedIp = available.get(0).getAddress().addr(); + return networkService.allocateIP(owner, dcId, network.getId(), null, requestedIp); + } + protected Pair getKubernetesClusterServerIpSshPortForIsolatedNetwork(Network network) { String ip = null; IpAddress address = getNetworkSourceNatIp(network); @@ -519,6 +589,27 @@ protected Pair getKubernetesClusterServerIpSshPortForVpcTier(Ne boolean acquireNewPublicIpForVpcTierIfNeeded) throws InsufficientAddressCapacityException, ResourceAllocationException, ResourceUnavailableException { int port = CLUSTER_NODES_DEFAULT_START_SSH_PORT; + if (manager.isNetrisNetwork(network)) { + // For Netris: + // NAT IP is the SSH/API/port-forwarding endpoint. + // LB IP (PUBLIC_IP_ID) is reserved for Kubernetes LoadBalancer + IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + if (natIp != null) { + return new Pair<>(natIp.getAddress().addr(), port); + } + if (acquireNewPublicIpForVpcTierIfNeeded) { + // Acquire LB IP first so it is stored under PUBLIC_IP_ID for Netris + if (getVpcTierKubernetesPublicIp(network) == null) { + acquireVpcTierKubernetesPublicIp(network, false); + } + natIp = acquireVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + if (natIp != null) { + return new Pair<>(natIp.getAddress().addr(), port); + } + } + logger.warn(String.format("No NAT public IP found for Netris VPC tier: %s, Kubernetes cluster: %s", network, kubernetesCluster.getName())); + return new Pair<>(null, port); + } IpAddress address = getVpcTierKubernetesPublicIp(network); if (address != null) { return new Pair<>(address.getAddress().addr(), port); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java index 62bd8b4576a4..bdf515a9bf40 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterDestroyWorker.java @@ -189,12 +189,27 @@ protected void deleteKubernetesClusterIsolatedNetworkRules(Network network, List } } - protected void deleteKubernetesClusterVpcTierRules(Network network, List removedVmIds) throws ManagementServerException { + protected void deleteKubernetesClusterVpcTierRules(Network network, List removedVmIds) throws ManagementServerException, ResourceUnavailableException { + removeVpcTierAclRules(network); + if (manager.isNetrisNetwork(network)) { + IpAddress lbIp = getVpcTierKubernetesPublicIp(network); + if (lbIp != null) { + removeLoadBalancingRule(lbIp, network, owner); + } + IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + if (natIp != null) { + try { + removePortForwardingRules(natIp, network, owner, removedVmIds); + } catch (ResourceUnavailableException e) { + throw new ManagementServerException(String.format("Failed to remove Kubernetes cluster port forwarding rules for network : %s", network.getName()), e); + } + } + return; + } IpAddress publicIp = getVpcTierKubernetesPublicIp(network); if (publicIp == null) { return; } - removeVpcTierAclRules(network); try { removePortForwardingRules(publicIp, network, owner, removedVmIds); } catch (ResourceUnavailableException e) { @@ -202,7 +217,7 @@ protected void deleteKubernetesClusterVpcTierRules(Network network, List r } } - private void deleteKubernetesClusterNetworkRules() throws ManagementServerException { + private void deleteKubernetesClusterNetworkRules() throws ManagementServerException, ResourceUnavailableException { NetworkVO network = networkDao.findById(kubernetesCluster.getNetworkId()); if (network == null) { return; @@ -242,7 +257,7 @@ private void validateClusterVMsDestroyed() { } } - private void checkForRulesToDelete() throws ManagementServerException { + private void checkForRulesToDelete() throws ManagementServerException, ResourceUnavailableException { NetworkVO kubernetesClusterNetwork = networkDao.findById(kubernetesCluster.getNetworkId()); if (kubernetesClusterNetwork != null && !manager.isDirectAccess(kubernetesClusterNetwork)) { deleteKubernetesClusterNetworkRules(); @@ -254,12 +269,18 @@ private void releaseVpcTierPublicIpIfNeeded() throws InsufficientAddressCapacity if (networkVO == null || networkVO.getVpcId() == null) { return; } - IpAddress address = getVpcTierKubernetesPublicIp(networkVO); - if (address == null) { - return; + IpAddress lbIp = getVpcTierKubernetesPublicIp(networkVO); + if (lbIp != null) { + networkService.releaseIpAddress(lbIp.getId()); + kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID); + } + if (manager.isNetrisNetwork(networkVO)) { + IpAddress natIp = getVpcTierKubernetesPublicIp(networkVO, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + if (natIp != null) { + networkService.releaseIpAddress(natIp.getId()); + kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + } } - networkService.releaseIpAddress(address.getId()); - kubernetesClusterDetailsDao.removeDetail(kubernetesCluster.getId(), ApiConstants.PUBLIC_IP_ID); } public boolean destroy() throws CloudRuntimeException { @@ -324,8 +345,8 @@ public boolean destroy() throws CloudRuntimeException { } else { try { checkForRulesToDelete(); - } catch (ManagementServerException e) { - String msg = String.format("Failed to remove network rules of Kubernetes cluster: %s", kubernetesCluster); + } catch (ManagementServerException | ResourceUnavailableException e) { + String msg = String.format("Failed to remove network rules of Kubernetes cluster : %s", kubernetesCluster.getName()); logger.warn(msg, e); updateKubernetesClusterEntryForGC(); throw new CloudRuntimeException(msg, e); diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java index 55924cb32e8a..573e752ce125 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterResourceModifierActionWorker.java @@ -799,6 +799,29 @@ protected void setupKubernetesClusterVpcTierRules(IpAddress publicIp, Network ne } } + + /** + * + * Netris-specific VPC tier rule setup. The two IPs serve distinct roles in Netris: + * lbIp (PUBLIC_IP_ID) — Kubernetes API (port 6443) via Netris L4 Load Balancer rule. + * natIp (NETRIS_NAT_PUBLIC_IP_ID) — SSH node access (ports 22001+) via Netris DNAT / port-forwarding. + */ + protected void setupKubernetesClusterVpcTierRulesForNetris(IpAddress lbIp, IpAddress natIp, + Network network, List clusterVMIds) throws ManagementServerException { + createVpcTierAclRules(network); + try { + provisionLoadBalancerRule(lbIp, network, owner, clusterVMIds, CLUSTER_API_PORT); + } catch (InsufficientAddressCapacityException | NetworkRuleConflictException e) { + throw new ManagementServerException(String.format("Failed to activate API load balancing rules for Netris Kubernetes cluster : %s", kubernetesCluster.getName()), e); + } + try { + Map vmIdPortMap = getVmPortMap(); + provisionSshPortForwardingRules(natIp, network, owner, clusterVMIds, vmIdPortMap); + } catch (ResourceUnavailableException | NetworkRuleConflictException e) { + throw new ManagementServerException(String.format("Failed to activate SSH port forwarding rules for Netris Kubernetes cluster : %s", kubernetesCluster.getName()), e); + } + } + /** * Generates a valid name prefix for Kubernetes cluster nodes. * diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java index 0ca27c29d8df..8f8d3aff87eb 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterScaleWorker.java @@ -32,6 +32,7 @@ import com.cloud.service.ServiceOfferingVO; import com.cloud.storage.VMTemplateVO; import org.apache.cloudstack.api.ApiCommandResourceType; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.context.CallContext; import org.apache.commons.collections.CollectionUtils; @@ -156,6 +157,25 @@ private void scaleKubernetesClusterIsolatedNetworkRules(final List cluster } private void scaleKubernetesClusterVpcTierRules(final List clusterVMIds) throws ManagementServerException { + if (manager.isNetrisNetwork(network)) { + // For Netris: SSH port-forwarding is on NAT IP; the LB rule on LB IP for 6443 is fixed and never re-provisioned during scale + IpAddress natIp = getVpcTierKubernetesPublicIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID); + if (natIp == null) { + throw new ManagementServerException(String.format("No NAT IP found for Netris VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName())); + } + try { + removePortForwardingRules(natIp, network, owner, CLUSTER_NODES_DEFAULT_START_SSH_PORT, CLUSTER_NODES_DEFAULT_START_SSH_PORT + clusterVMIds.size() - 1); + } catch (ResourceUnavailableException e) { + throw new ManagementServerException(String.format("Failed to remove SSH port forwarding rules for the Kubernetes cluster : %s", kubernetesCluster.getName()), e); + } + try { + Map vmIdPortMap = getVmPortMap(); + provisionSshPortForwardingRules(natIp, network, owner, clusterVMIds, vmIdPortMap); + } catch (ResourceUnavailableException | NetworkRuleConflictException e) { + throw new ManagementServerException(String.format("Failed to activate SSH port forwarding rules for the Kubernetes cluster : %s", kubernetesCluster.getName()), e); + } + return; + } IpAddress publicIp = getVpcTierKubernetesPublicIp(network); if (publicIp == null) { throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName())); @@ -165,7 +185,6 @@ private void scaleKubernetesClusterVpcTierRules(final List clusterVMIds) t } catch (ResourceUnavailableException e) { throw new ManagementServerException(String.format("Failed to remove SSH port forwarding rules for removed VMs for the Kubernetes cluster : %s", kubernetesCluster.getName()), e); } - // Add port forwarding rule for SSH access on each node VM try { Map vmIdPortMap = getVmPortMap(); provisionSshPortForwardingRules(publicIp, network, owner, clusterVMIds, vmIdPortMap); @@ -479,7 +498,7 @@ private void scaleUpKubernetesClusterSize(final long newVmCount) throws CloudRun launchPermissionDao.persist(launchPermission); } try { - clusterVMs = provisionKubernetesClusterNodeVms((int)(newVmCount + kubernetesCluster.getNodeCount()), (int)kubernetesCluster.getNodeCount(), publicIpAddress, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId()); + clusterVMs = provisionKubernetesClusterNodeVms((int)(newVmCount + kubernetesCluster.getNodeCount()), (int)kubernetesCluster.getNodeCount(), apiPublicIpAddress, kubernetesCluster.getDomainId(), kubernetesCluster.getAccountId()); updateLoginUserDetails(clusterVMs.stream().map(InternalIdentity::getId).collect(Collectors.toList())); } catch (CloudRuntimeException | ManagementServerException | ResourceUnavailableException | InsufficientCapacityException e) { logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to provision node VM in the cluster", kubernetesCluster.getName()), e); @@ -518,6 +537,9 @@ private void scaleKubernetesClusterSize(KubernetesClusterNodeType nodeType) thro Pair publicIpSshPort = getKubernetesClusterServerIpSshPort(null); publicIpAddress = publicIpSshPort.first(); sshPort = publicIpSshPort.second(); + // For Netris VPC-tier: apiPublicIpAddress = LB IP (kubeadm join target for new nodes). + // For all others: same as publicIpAddress. + apiPublicIpAddress = resolveApiPublicIpAddress(network); if (StringUtils.isEmpty(publicIpAddress)) { logTransitStateToFailedIfNeededAndThrow(Level.ERROR, String.format("Scaling failed for Kubernetes cluster : %s, unable to retrieve associated public IP", kubernetesCluster.getName())); } diff --git a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java index aa9317e619b0..4b7f3c672258 100644 --- a/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java +++ b/plugins/integrations/kubernetes-service/src/main/java/com/cloud/kubernetes/cluster/actionworkers/KubernetesClusterStartWorker.java @@ -38,6 +38,7 @@ import com.cloud.network.vpc.NetworkACL; import com.cloud.storage.VMTemplateVO; import com.cloud.user.UserDataVO; +import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.api.BaseCmd; import org.apache.cloudstack.api.InternalIdentity; import org.apache.cloudstack.framework.ca.Certificate; @@ -103,6 +104,31 @@ public KubernetesSupportedVersion getKubernetesClusterVersion() { return kubernetesClusterVersion; } + /** + * Returns the VPC-tier public IP stored under {@code detailKey} for this cluster, + * acquiring a new one if absent. Throws {@link ManagementServerException} when + * neither the lookup nor the allocation succeeds. + */ + private IpAddress resolveOrAcquireVpcTierIp(Network network, String detailKey, String role) + throws ManagementServerException, InsufficientAddressCapacityException, ResourceUnavailableException { + IpAddress ip = getVpcTierKubernetesPublicIp(network, detailKey); + if (ip == null) { + try { + ip = acquireVpcTierKubernetesPublicIp(network, detailKey); + } catch (ResourceAllocationException e) { + throw new ManagementServerException(String.format( + "Failed to acquire %s IP for VPC tier : %s, Kubernetes cluster : %s", + role, network.getName(), kubernetesCluster.getName()), e); + } + } + if (ip == null) { + throw new ManagementServerException(String.format( + "Failed to acquire %s IP for VPC tier : %s, Kubernetes cluster : %s", + role, network.getName(), kubernetesCluster.getName())); + } + return ip; + } + private Pair> getKubernetesControlNodeIpAddresses(final DataCenter zone, final Network network, final Account account) throws InsufficientAddressCapacityException { String controlNodeIp = null; Map requestedIps = null; @@ -608,7 +634,7 @@ private Network startKubernetesClusterNetwork(final DeployDestination destinatio return network; } - protected void setupKubernetesClusterNetworkRules(Network network, List clusterVMs) throws ManagementServerException { + protected void setupKubernetesClusterNetworkRules(Network network, List clusterVMs) throws ManagementServerException, InsufficientAddressCapacityException, ResourceUnavailableException { if (manager.isDirectAccess(network)) { if (logger.isDebugEnabled()) { logger.debug("Network: {} for Kubernetes cluster: {} is not an isolated network or ROUTED network, therefore, no need for network rules", network, kubernetesCluster); @@ -617,11 +643,14 @@ protected void setupKubernetesClusterNetworkRules(Network network, List } List clusterVMIds = clusterVMs.stream().map(UserVm::getId).collect(Collectors.toList()); if (network.getVpcId() != null) { - IpAddress publicIp = getVpcTierKubernetesPublicIp(network); - if (publicIp == null) { - throw new ManagementServerException(String.format("No public IP addresses found for VPC tier : %s, Kubernetes cluster : %s", network.getName(), kubernetesCluster.getName())); + if (manager.isNetrisNetwork(network)) { + IpAddress natIp = resolveOrAcquireVpcTierIp(network, ApiConstants.NETRIS_NAT_PUBLIC_IP_ID, "NAT"); + IpAddress lbIp = resolveOrAcquireVpcTierIp(network, ApiConstants.PUBLIC_IP_ID, "LB"); + setupKubernetesClusterVpcTierRulesForNetris(lbIp, natIp, network, clusterVMIds); + } else { + IpAddress lbIp = resolveOrAcquireVpcTierIp(network, ApiConstants.PUBLIC_IP_ID, "public"); + setupKubernetesClusterVpcTierRules(lbIp, network, clusterVMIds); } - setupKubernetesClusterVpcTierRules(publicIp, network, clusterVMIds); return; } IpAddress publicIp = getNetworkSourceNatIp(network); @@ -711,7 +740,7 @@ private boolean isKubernetesClusterKubeConfigAvailable(final long timeoutTime) { final String controlVMPrivateIpAddress = getControlVmPrivateIp(); if (StringUtils.isNotEmpty(controlVMPrivateIpAddress)) { kubeConfig = kubeConfig.replace(String.format("server: https://%s:%d", controlVMPrivateIpAddress, CLUSTER_API_PORT), - String.format("server: https://%s:%d", publicIpAddress, CLUSTER_API_PORT)); + String.format("server: https://%s:%d", apiPublicIpAddress, CLUSTER_API_PORT)); } kubernetesClusterDetailsDao.addDetail(kubernetesCluster.getId(), "kubeConfigData", Base64.encodeBase64String(kubeConfig.getBytes(com.cloud.utils.StringUtils.getPreferredCharset())), false); return true; @@ -735,7 +764,7 @@ private boolean isKubernetesClusterDashboardServiceRunning(final boolean onCreat private void updateKubernetesClusterEntryEndpoint() { KubernetesClusterVO kubernetesClusterVO = kubernetesClusterDao.findById(kubernetesCluster.getId()); - kubernetesClusterVO.setEndpoint(String.format("https://%s:%d/", publicIpAddress, CLUSTER_API_PORT)); + kubernetesClusterVO.setEndpoint(String.format("https://%s:%d/", apiPublicIpAddress, CLUSTER_API_PORT)); kubernetesClusterDao.update(kubernetesCluster.getId(), kubernetesClusterVO); } @@ -767,6 +796,7 @@ public boolean startKubernetesClusterOnCreate(Long domainId, Long accountId, Lon logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as failed to acquire public IP" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed); } publicIpAddress = publicIpSshPort.first(); + apiPublicIpAddress = resolveApiPublicIpAddress(network); if (StringUtils.isEmpty(publicIpAddress) && (!manager.isDirectAccess(network) || kubernetesCluster.getControlNodeCount() > 1)) { // Shared network, single-control node cluster won't have an IP yet logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed); @@ -789,7 +819,7 @@ public boolean startKubernetesClusterOnCreate(Long domainId, Long accountId, Lon Pair k8sControlVMAndIp = new Pair<>(null, null); UserVm k8sControlVM = null; try { - k8sControlVMAndIp = provisionKubernetesClusterControlVm(network, publicIpAddress, etcdGuestNodeIps, domainId, accountId, asNumber); + k8sControlVMAndIp = provisionKubernetesClusterControlVm(network, apiPublicIpAddress, etcdGuestNodeIps, domainId, accountId, asNumber); } catch (CloudRuntimeException | ManagementServerException | ResourceUnavailableException | InsufficientCapacityException e) { logTransitStateAndThrow(Level.ERROR, String.format("Provisioning the control VM failed in the Kubernetes cluster : %s", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed, e); } @@ -798,6 +828,7 @@ public boolean startKubernetesClusterOnCreate(Long domainId, Long accountId, Lon if (StringUtils.isEmpty(publicIpAddress)) { publicIpSshPort = getKubernetesClusterServerIpSshPort(k8sControlVM); publicIpAddress = publicIpSshPort.first(); + apiPublicIpAddress = resolveApiPublicIpAddress(network); if (StringUtils.isEmpty(publicIpAddress)) { logTransitStateAndThrow(Level.WARN, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed); } @@ -819,7 +850,7 @@ public boolean startKubernetesClusterOnCreate(Long domainId, Long accountId, Lon } try { setupKubernetesClusterNetworkRules(network, clusterVMs); - } catch (ManagementServerException e) { + } catch (ManagementServerException | InsufficientAddressCapacityException | ResourceUnavailableException e) { logTransitStateAndThrow(Level.ERROR, String.format("Failed to setup Kubernetes cluster : %s, unable to setup network rules", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.CreateFailed, e); } try { @@ -840,7 +871,7 @@ public boolean startKubernetesClusterOnCreate(Long domainId, Long accountId, Lon } logTransitStateDetachIsoAndThrow(Level.ERROR, msg, kubernetesCluster, clusterVMs, KubernetesCluster.Event.CreateFailed, null); } - boolean k8sApiServerSetup = KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, publicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000); + boolean k8sApiServerSetup = KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000); if (!k8sApiServerSetup) { logTransitStateDetachIsoAndThrow(Level.ERROR, String.format("Failed to setup Kubernetes cluster : %s in usable state as unable to provision API endpoint for the cluster", kubernetesCluster.getName()), kubernetesCluster, clusterVMs, KubernetesCluster.Event.CreateFailed, null); } @@ -886,10 +917,13 @@ public boolean startStoppedKubernetesCluster(Long domainId, Long accountId) thro Pair sshIpPort = getKubernetesClusterServerIpSshPort(null); publicIpAddress = sshIpPort.first(); sshPort = sshIpPort.second(); + Network network = networkDao.findById(kubernetesCluster.getNetworkId()); + apiPublicIpAddress = resolveApiPublicIpAddress(network); if (StringUtils.isEmpty(publicIpAddress)) { logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s as no public IP found for the cluster" , kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.OperationFailed); } - if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, publicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000)) { + // API check uses apiPublicIpAddress (LB IP for Netris) + if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress, CLUSTER_API_PORT, startTimeoutTime, 15000)) { logTransitStateAndThrow(Level.ERROR, String.format("Failed to start Kubernetes cluster : %s in usable state", kubernetesCluster.getName()), kubernetesCluster.getId(), KubernetesCluster.Event.OperationFailed); } if (!isKubernetesClusterKubeConfigAvailable(startTimeoutTime)) { @@ -915,6 +949,8 @@ public boolean reconcileAlertCluster() { Pair sshIpPort = getKubernetesClusterServerIpSshPort(null); publicIpAddress = sshIpPort.first(); sshPort = sshIpPort.second(); + Network network = networkDao.findById(kubernetesCluster.getNetworkId()); + apiPublicIpAddress = resolveApiPublicIpAddress(network); if (StringUtils.isEmpty(publicIpAddress)) { return false; } @@ -930,7 +966,8 @@ public boolean reconcileAlertCluster() { if (StringUtils.isEmpty(sshIpPort.first())) { return false; } - if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, sshIpPort.first(), + // API server check uses apiPublicIpAddress (LB IP for Netris) + if (!KubernetesClusterUtil.isKubernetesClusterServerRunning(kubernetesCluster, apiPublicIpAddress, KubernetesClusterActionWorker.CLUSTER_API_PORT, startTimeoutTime, 0)) { return false; } diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java index e700b3b9d2d1..dfca9bade7d5 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommand.java @@ -16,6 +16,9 @@ // under the License. package org.apache.cloudstack.agent.api; +import com.cloud.offering.NetworkOffering; +import com.cloud.utils.net.NetUtils; + public class CreateNetrisVnetCommand extends NetrisCommand { private String vpcName; private Long vpcId; @@ -24,7 +27,8 @@ public class CreateNetrisVnetCommand extends NetrisCommand { private String gateway; private String netrisTag; private String ipv6Cidr; - private Boolean globalRouting; + private NetworkOffering.NetworkMode networkMode; + private NetUtils.InternetProtocol internetProtocol; public CreateNetrisVnetCommand(Long zoneId, Long accountId, Long domainId, String vpcName, Long vpcId, String vNetName, Long networkId, String cidr, String gateway, boolean isVpc) { super(zoneId, accountId, domainId, vNetName, networkId, isVpc); @@ -74,11 +78,19 @@ public void setIpv6Cidr(String ipv6Cidr) { this.ipv6Cidr = ipv6Cidr; } - public Boolean isGlobalRouting() { - return globalRouting; + public NetworkOffering.NetworkMode getNetworkMode() { + return networkMode; + } + + public void setNetworkMode(NetworkOffering.NetworkMode networkMode) { + this.networkMode = networkMode; + } + + public NetUtils.InternetProtocol getInternetProtocol() { + return internetProtocol; } - public void setGlobalRouting(Boolean globalRouting) { - this.globalRouting = globalRouting; + public void setInternetProtocol(NetUtils.InternetProtocol internetProtocol) { + this.internetProtocol = internetProtocol; } } diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java index 280c7de32d1e..3c14ed764f17 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/agent/api/NetrisCommand.java @@ -25,6 +25,7 @@ public class NetrisCommand extends Command { private String name; private final Long id; private final boolean isVpc; + private boolean isL2; public NetrisCommand(long zoneId, Long accountId, Long domainId, String name, Long id, boolean isVpc) { this.zoneId = zoneId; @@ -63,6 +64,14 @@ public boolean isVpc() { return isVpc; } + public boolean isL2() { + return isL2; + } + + public void setL2(boolean l2) { + isL2 = l2; + } + @Override public boolean executeInSequence() { return false; diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java index 164c56cd763b..5b7b9d8973dd 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/resource/NetrisResourceObjectUtils.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.resource; +import com.cloud.exception.InvalidParameterValueException; +import com.cloud.offering.NetworkOffering; import org.apache.cloudstack.agent.api.NetrisCommand; import org.apache.commons.lang3.ArrayUtils; @@ -23,6 +25,8 @@ public class NetrisResourceObjectUtils { + public static final int NETRIS_VNET_NAME_MAX_LENGTH = 50; + public enum NetrisObjectType { VPC, IPAM_ALLOCATION, IPAM_SUBNET, VNET, SNAT, STATICNAT, DNAT, STATICROUTE, ACL, LB } @@ -73,7 +77,7 @@ public static String retrieveNetrisResourceObjectName(NetrisCommand cmd, NetrisO break; case STATICNAT: stringBuilder.append(String.format("%s%s-VM%s-%s", prefix, suffixes[0], suffixes[1], "STATICNAT")); - suffixes = new String[0]; + suffixes = ArrayUtils.subarray(suffixes, 2, suffixes.length); break; case DNAT: stringBuilder.append(String.format("%s%s-%s", prefix, suffixes[0], "DNAT")); @@ -100,4 +104,17 @@ public static String retrieveNetrisResourceObjectName(NetrisCommand cmd, NetrisO } return stringBuilder.toString(); } + + public static void validateNetrisVnetNameLength(String netrisVnetName, String networkName) { + if (netrisVnetName.length() > NETRIS_VNET_NAME_MAX_LENGTH) { + throw new InvalidParameterValueException( + String.format("The resulting Netris vNet name '%s' exceeds the maximum allowed length of %d characters (actual: %d). " + + "Please use a shorter network name (current: '%s')", + netrisVnetName, NETRIS_VNET_NAME_MAX_LENGTH, netrisVnetName.length(), networkName)); + } + } + + public static NetworkOffering.NetworkMode getNetworkMode(NetworkOffering.NetworkMode mode) { + return Objects.isNull(mode) ? NetworkOffering.NetworkMode.NATTED : mode; + } } diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java index 9d3ff953ee15..b053f2a854eb 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java @@ -21,6 +21,7 @@ import com.cloud.network.netris.NetrisNetworkRule; import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.StaticRouteVO; +import com.cloud.offering.NetworkOffering; import com.cloud.user.Account; import com.cloud.utils.Pair; import com.cloud.utils.exception.CloudRuntimeException; @@ -108,6 +109,7 @@ import io.netris.model.VnetResDeleteBody; import io.netris.model.VnetResListBody; import io.netris.model.VnetsBody; +import io.netris.model.VnetsBodyGateways; import io.netris.model.VpcEditResponseOK; import io.netris.model.VpcVpcIdBody; import io.netris.model.response.AuthResponse; @@ -329,6 +331,11 @@ private InlineResponse2004Data createIpamAllocationInternal(String ipamName, Str @Override public boolean createVpc(CreateNetrisVpcCommand cmd) { String netrisVpcName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VPC); + VPCListing existingNetrisVpc = getVpcByNameAndTenant(netrisVpcName); + if (existingNetrisVpc != null) { + logger.info("Netris VPC {} already exists, skipping creation", netrisVpcName); + return true; + } VPCResponseObjectOK createdVpc = createVpcInternal(netrisVpcName, tenantId, tenantName); if (createdVpc == null || !createdVpc.isIsSuccess()) { String reason = createdVpc == null ? "Empty response" : "Operation failed on Netris"; @@ -338,8 +345,19 @@ public boolean createVpc(CreateNetrisVpcCommand cmd) { String netrisIpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, cmd.getCidr()); String vpcCidr = cmd.getCidr(); - InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisIpamAllocationName, vpcCidr, createdVpc.getData()); - return createdIpamAllocation != null; + VPCListing vpcListing = createdVpc.getData(); + InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisIpamAllocationName, vpcCidr, vpcListing); + if (createdIpamAllocation == null) { + logger.warn("IPAM Allocation creation failed for VPC {}, rolling back VPC", netrisVpcName); + try { + deleteVpcInternal(vpcListing); + } catch (Exception e) { + logger.error("Failed to rollback VPC {} after IPAM Allocation failure - manual cleanup may be required: {}", + netrisVpcName, e.getMessage()); + } + return false; + } + return true; } @Override @@ -352,9 +370,13 @@ public boolean updateVpc(UpdateNetrisVpcCommand cmd) { String netrisVpcName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VPC); String netrisPrevVpcName = String.format("D%s-A%s-Z%s-V%s-%s", domainId, accountId, zoneId, vpcId, prevVpcName); VpcEditResponseOK updatedVpc = updateVpcInternal(netrisVpcName, netrisPrevVpcName, tenantId, tenantName); + return validateNetrisVpcUpdateResponse(updatedVpc, prevVpcName); + } + + private boolean validateNetrisVpcUpdateResponse(VpcEditResponseOK updatedVpc, String previousVpcName) { if (updatedVpc == null || !updatedVpc.isIsSuccess()) { String reason = updatedVpc == null ? "Empty response" : "Operation failed on Netris"; - logger.debug("The update of Netris VPC {} failed: {}", cmd.getPreviousVpcName(), reason); + logger.debug("The update of Netris VPC {} failed: {}", previousVpcName, reason); return false; } return true; @@ -372,6 +394,17 @@ public boolean deleteNatRule(DeleteNetrisNatRuleCommand cmd) { } String natRuleName = cmd.getNatRuleName(); NatGetBody existingNatRule = netrisNatRuleExists(natRuleName); + // Backward compatibility: rules created before the public-IP suffix was added use the legacy name + if (existingNatRule == null && "STATICNAT".equals(cmd.getNatRuleType())) { + String legacyName = getLegacyStaticNatRuleName(natRuleName); + if (legacyName != null) { + logger.debug("Static NAT rule not found with name '{}', falling back to legacy name '{}'", natRuleName, legacyName); + existingNatRule = netrisNatRuleExists(legacyName); + if (existingNatRule != null) { + natRuleName = legacyName; + } + } + } boolean ruleExists = Objects.nonNull(existingNatRule); if (ruleExists) { deleteNatRule(natRuleName, existingNatRule.getId(), vpcResource.getName()); @@ -416,7 +449,7 @@ public boolean addOrUpdateAclRule(CreateOrUpdateNetrisACLCommand cmd, boolean fo try { aclApi.apiAclPut(aclEditItem); } catch (ApiException e) { - if (e.getResponseBody().contains("This kind of acl already exists")) { + if (e.getResponseBody().contains("already exists")) { logger.info("Netris ACL rule: {} already exists and doesn't need to be updated", aclName); return true; } @@ -426,7 +459,15 @@ public boolean addOrUpdateAclRule(CreateOrUpdateNetrisACLCommand cmd, boolean fo } AclAddItem aclAddItem = getAclAddItem(cmd, aclName); aclAddItem.setVpc(vpc); - aclApi.apiAclPost(aclAddItem); + try { + aclApi.apiAclPost(aclAddItem); + } catch (ApiException e) { + if (e.getResponseBody() != null && e.getResponseBody().contains("already exists")) { + logger.info("Netris ACL rule: {} already exists, skipping creation", aclName); + return true; + } + throw e; + } } catch (ApiException e) { logAndThrowException(String.format("Failed to create Netris ACL: %s", cmd.getNetrisAclName()), e); } @@ -949,13 +990,12 @@ public boolean deleteLbRule(DeleteNetrisLoadBalancerRuleCommand cmd) { logger.error("Could not find the Netris LB rule with name {}", lbName); return false; } - if (matchingLbId.isEmpty()) { + if (!matchingLbId.isEmpty()) { + L4LoadBalancerApi lbApi = apiClient.getApiStubForMethod(L4LoadBalancerApi.class); + lbApi.apiV2L4lbIdDelete(matchingLbId.get(0).intValue()); + } else { logger.warn("There doesn't seem to be any LB rule on Netris matching {}", lbName); - return true; } - - L4LoadBalancerApi lbApi = apiClient.getApiStubForMethod(L4LoadBalancerApi.class); - lbApi.apiV2L4lbIdDelete(matchingLbId.get(0).intValue()); if (Objects.nonNull(cidrList)) { deleteAclRulesForLb(cmd); } @@ -1104,6 +1144,32 @@ private VPCResourceIpam getVpcAllocationResource(VPCResponseResourceOK vpcResour return null; } + private String getL2VpcName(NetrisCommand cmd, String name) { + return String.format("D%s-A%s-Z%s-N%s-%s", + cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), cmd.getId(), name); + } + + /** + * Returns the existing dedicated Netris VPC for an L2 network, creating it if it does not yet exist. + * The VPC is created with no IPAM allocation. + */ + private VPCListing getOrCreateL2Vpc(NetrisCommand cmd) { + String l2VpcName = getL2VpcName(cmd, cmd.getName()); + VPCListing existing = getVpcByNameAndTenant(l2VpcName); + if (existing != null) { + logger.debug("Found existing dedicated L2 VPC {} on Netris", l2VpcName); + return existing; + } + logger.debug("Creating dedicated L2 VPC {} on Netris (no subnet)", l2VpcName); + VPCResponseObjectOK created = createVpcInternal(l2VpcName, tenantId, tenantName); + if (created == null || !created.isIsSuccess()) { + logger.error("Failed to create dedicated L2 VPC {} on Netris", l2VpcName); + return null; + } + + return getVpcByNameAndTenant(l2VpcName); + } + private VPCListing getVpcByNameAndTenant(String vpcName) { try { List vpcListings = listVPCs(); @@ -1159,18 +1225,52 @@ public boolean createVnet(CreateNetrisVnetCommand cmd) { String vnetCidr = cmd.getCidr(); Integer vxlanId = cmd.getVxlanId(); String netrisTag = cmd.getNetrisTag(); - String netmask = vnetCidr.split("/")[1]; - String netrisGateway = cmd.getGateway() + "/" + netmask; - String netrisV6Cidr = cmd.getIpv6Cidr(); boolean isVpc = cmd.isVpc(); - Boolean isGlobalRouting = cmd.isGlobalRouting(); + boolean isL2 = cmd.isL2(); + + // Derive per-address-family global routing flags from networkMode + internetProtocol: + // ipv4 NAT -> ipv4 global routing: false, ipv6 global routing: n/a + // ipv4 ROUTED -> ipv4 global routing: true, ipv6 global routing: n/a + // dual NAT -> ipv4 global routing: false, ipv6 global routing: true + // dual ROUTED -> ipv4 global routing: true, ipv6 global routing: true + NetworkOffering.NetworkMode networkMode = cmd.getNetworkMode(); + NetUtils.InternetProtocol internetProtocol = cmd.getInternetProtocol(); + Boolean ipv4GlobalRouting = null; + Boolean ipv6GlobalRouting = null; + if (networkMode != null) { + ipv4GlobalRouting = NetworkOffering.NetworkMode.ROUTED.equals(networkMode); + if (NetUtils.InternetProtocol.DualStack.equals(internetProtocol)) { + ipv6GlobalRouting = true; + } + } + + String netrisGateway = null; + if (!isL2 && vnetCidr != null && cmd.getGateway() != null) { + String netmask = vnetCidr.split("/")[1]; + netrisGateway = cmd.getGateway() + "/" + netmask; + } + String netrisV6Cidr = cmd.getIpv6Cidr(); + + VPCListing associatedVpc = null; + String netrisSubnetName = null; + String netrisV6IpamAllocationName = null; + String netrisV6SubnetName = null; + boolean createdIpv6Allocation = false; try { - String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); - VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName); - if (associatedVpc == null) { - logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName); - return false; + if (isL2) { + associatedVpc = getOrCreateL2Vpc(cmd); + if (associatedVpc == null) { + logger.error("Failed to get or create dedicated L2 VPC to create the corresponding vNet for L2 network {}", networkName); + return false; + } + } else { + String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); + associatedVpc = getNetrisVpcResource(netrisVpcName); + if (associatedVpc == null) { + logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName); + return false; + } } String vNetName; @@ -1179,31 +1279,44 @@ public boolean createVnet(CreateNetrisVnetCommand cmd) { } else { vNetName = String.format("N%s-%s", networkId, networkName); } - String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ; - String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr) ; - - createIpamSubnetInternal(netrisSubnetName, vnetCidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, isGlobalRouting); - if (Objects.nonNull(netrisV6Cidr)) { - String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr); - String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), netrisV6Cidr) ; - BigDecimal ipamAllocationId = getIpamAllocationIdByPrefixAndVpc(netrisV6Cidr, associatedVpc); - if (ipamAllocationId == null) { - InlineResponse2004Data createdIpamAllocation = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc); - if (Objects.isNull(createdIpamAllocation)) { - throw new CloudRuntimeException(String.format("Failed to create Netris IPAM Allocation %s for VPC %s", netrisV6IpamAllocationName, netrisVpcName)); + String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName); + NetrisResourceObjectUtils.validateNetrisVnetNameLength(netrisVnetName, networkName); + + if (!isL2) { + netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr); + createIpamSubnetInternal(netrisSubnetName, vnetCidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv4GlobalRouting); + if (Objects.nonNull(netrisV6Cidr)) { + netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, netrisV6Cidr); + netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), netrisV6Cidr); + BigDecimal ipamAllocationId = getIpamAllocationIdByPrefixAndVpc(netrisV6Cidr, associatedVpc); + if (ipamAllocationId == null) { + InlineResponse2004Data createdIpamAllocationData = createIpamAllocationInternal(netrisV6IpamAllocationName, netrisV6Cidr, associatedVpc); + if (Objects.isNull(createdIpamAllocationData)) { + rollbackVnetResources(associatedVpc, netrisSubnetName, null, null, networkName); + throw new CloudRuntimeException(String.format("Failed to create Netris IPAM Allocation %s for VPC %s", netrisV6IpamAllocationName, associatedVpc.getName())); + } + createdIpv6Allocation = true; } + createIpamSubnetInternal(netrisV6SubnetName, netrisV6Cidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, ipv6GlobalRouting); } - createIpamSubnetInternal(netrisV6SubnetName, netrisV6Cidr, SubnetBody.PurposeEnum.COMMON, associatedVpc, isGlobalRouting); + logger.debug("Successfully created IPAM Subnet for network {} on Netris", networkName); } - logger.debug("Successfully created IPAM Subnet {} for network {} on Netris", netrisSubnetName, networkName); - VnetResAddBody vnetResponse = createVnetInternal(associatedVpc, netrisVnetName, netrisGateway, netrisV6Cidr, vxlanId, netrisTag); + VnetResAddBody vnetResponse = createVnetInternal(associatedVpc, netrisVnetName, netrisGateway, isL2 ? null : netrisV6Cidr, vxlanId, netrisTag); if (vnetResponse == null || !vnetResponse.isIsSuccess()) { String reason = vnetResponse == null ? "Empty response" : "Operation failed on Netris"; logger.debug("The Netris vNet creation {} failed: {}", vNetName, reason); + if (!isL2) { + rollbackVnetResources(associatedVpc, netrisSubnetName, netrisV6SubnetName, + createdIpv6Allocation ? netrisV6IpamAllocationName : null, networkName); + } return false; } - } catch (ApiException e) { + } catch (Exception e) { + if (!isL2 && associatedVpc != null) { + rollbackVnetResources(associatedVpc, netrisSubnetName, netrisV6SubnetName, + createdIpv6Allocation ? netrisV6IpamAllocationName : null, networkName); + } throw new CloudRuntimeException(String.format("Failed to create Netris vNet %s", networkName), e); } return true; @@ -1217,11 +1330,12 @@ public boolean updateVnet(UpdateNetrisVnetCommand cmd) { String vpcName = cmd.getVpcName(); Long vpcId = cmd.getVpcId(); boolean isVpc = cmd.isVpc(); + boolean isL2 = cmd.isL2(); + + VPCListing associatedVpc = getAssociatedVPC(cmd); - String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); - VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName); if (associatedVpc == null) { - logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName); + logger.error("Failed to find Netris VPC with name: {}, to update the corresponding vNet for network {}", vpcName, networkName); return false; } @@ -1237,15 +1351,39 @@ public boolean updateVnet(UpdateNetrisVnetCommand cmd) { String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ; String prevNetrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, prevVnetName) ; + NetrisResourceObjectUtils.validateNetrisVnetNameLength(netrisVnetName, networkName); VnetResAddBody response = updateVnetInternal(associatedVpc, netrisVnetName, prevNetrisVnetName); if (response == null || !response.isIsSuccess()) { String reason = response == null ? "Empty response" : "Operation failed on Netris"; logger.debug("Netris vNet: {} update failed: {}", vNetName, reason); return false; } + if (isL2) { + String netrisNewVpcName = String.format("D%s-A%s-Z%s-N%s-%s", cmd.getDomainId(), cmd.getAccountId(), cmd.getZoneId(), networkId, networkName); + VpcEditResponseOK updatedVpc = updateVpcInternal(netrisNewVpcName, associatedVpc.getName(), tenantId, tenantName); + return validateNetrisVpcUpdateResponse(updatedVpc, associatedVpc.getName()); + } return true; } + private VPCListing getAssociatedVPC(UpdateNetrisVnetCommand cmd) { + VPCListing associatedVpc; + boolean isL2 = cmd.isL2(); + if (isL2) { + String l2VpcName = getL2VpcName(cmd, cmd.getPrevNetworkName()); + associatedVpc = getVpcByNameAndTenant(l2VpcName); + if (associatedVpc == null) { + logger.error("Failed to find dedicated L2 VPC {} to update the corresponding vNet for L2 network {}", l2VpcName, cmd.getName()); + } + } else { + Long vpcId = cmd.getVpcId(); + String vpcName = cmd.getVpcName(); + String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); + associatedVpc = getNetrisVpcResource(netrisVpcName); + } + return associatedVpc; + } + private VnetResAddBody updateVnetInternal(VPCListing associatedVpc, String netrisVnetName, String prevVnetName) { logger.debug("Updating Netris vNet name from {} to {} ", netrisVnetName, prevVnetName); try { @@ -1263,24 +1401,27 @@ private VnetResAddBody updateVnetInternal(VPCListing associatedVpc, String netri vnetBody.setCustomAnycastMac(vnetBody.getCustomAnycastMac()); - VnetEditBodyGateways gatewayV4 = new VnetEditBodyGateways(); - gatewayV4.prefix(vnetsBody.getGateways().get(0).getPrefix()); - gatewayV4.setDhcpEnabled(false); - VnetEditBodyDhcp dhcp = new VnetEditBodyDhcp(); - dhcp.setEnd(""); - dhcp.setStart(""); - dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet()); - gatewayV4.setDhcp(dhcp); List gatewaysList = new ArrayList<>(); - gatewaysList.add(gatewayV4); - - if (vnetsBody.getGateways().size() > 1 && Objects.nonNull(vnetsBody.getGateways().get(1))) { - String netrisV6Gateway = vnetsBody.getGateways().get(1).getPrefix(); - VnetEditBodyGateways gatewayV6 = new VnetEditBodyGateways(); - gatewayV6.prefix(netrisV6Gateway); - gatewayV6.setDhcpEnabled(false); - gatewayV6.setDhcp(dhcp); - gatewaysList.add(gatewayV6); + List existingGateways = vnetsBody.getGateways(); + if (!CollectionUtils.isEmpty(existingGateways)) { + VnetEditBodyDhcp dhcp = new VnetEditBodyDhcp(); + dhcp.setEnd(""); + dhcp.setStart(""); + dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet()); + + VnetEditBodyGateways gatewayV4 = new VnetEditBodyGateways(); + gatewayV4.prefix(vnetsBody.getGateways().get(0).getPrefix()); + gatewayV4.setDhcpEnabled(false); + gatewayV4.setDhcp(dhcp); + gatewaysList.add(gatewayV4); + + if (existingGateways.size() > 1 && Objects.nonNull(vnetsBody.getGateways().get(1))) { + VnetEditBodyGateways gatewayV6 = new VnetEditBodyGateways(); + gatewayV6.prefix(vnetsBody.getGateways().get(1).getPrefix()); + gatewayV6.setDhcpEnabled(false); + gatewayV6.setDhcp(dhcp); + gatewaysList.add(gatewayV6); + } } vnetBody.setGateways(gatewaysList); @@ -1333,14 +1474,25 @@ public boolean deleteVnet(DeleteNetrisVnetCommand cmd) { String networkName = cmd.getName(); Long networkId = cmd.getId(); boolean isVpc = cmd.isVpc(); + boolean isL2 = cmd.isL2(); String vnetCidr = cmd.getVNetCidr(); String vnetV6Cidr = cmd.getvNetV6Cidr(); try { - String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); - VPCListing associatedVpc = getNetrisVpcResource(netrisVpcName); - if (associatedVpc == null) { - logger.error("Failed to find Netris VPC with name: {}, to create the corresponding vNet for network {}", netrisVpcName, networkName); - return false; + VPCListing associatedVpc; + if (isL2) { + String l2VpcName = getL2VpcName(cmd, networkName); + associatedVpc = getVpcByNameAndTenant(l2VpcName); + if (associatedVpc == null) { + logger.warn("Dedicated L2 VPC {} not found on Netris — vNet for network {} may have already been deleted", l2VpcName, networkName); + return true; + } + } else { + String netrisVpcName = getNetrisVpcName(cmd, vpcId, vpcName); + associatedVpc = getNetrisVpcResource(netrisVpcName); + if (associatedVpc == null) { + logger.error("Failed to find Netris VPC with name: {}, to delete the corresponding vNet for network {}", netrisVpcName, networkName); + return false; + } } String vNetName; @@ -1350,19 +1502,28 @@ public boolean deleteVnet(DeleteNetrisVnetCommand cmd) { vNetName = String.format("N%s-%s", networkId, networkName); } - String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName) ; - String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr); + String netrisVnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.VNET, vNetName); FilterByVpc vpcFilter = new FilterByVpc(); vpcFilter.add(associatedVpc.getId()); deleteVnetInternal(associatedVpc, vpcFilter, netrisVnetName, vNetName); - logger.debug("Successfully deleted vNet {}", vNetName); - deleteSubnetInternal(vpcFilter, netrisVnetName, netrisSubnetName); - if (Objects.nonNull(vnetV6Cidr)) { - String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, vnetV6Cidr); - String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetV6Cidr); - deleteSubnetInternal(vpcFilter, netrisVnetName, netrisV6SubnetName); - deleteVpcIpamAllocationInternal(associatedVpc, netrisV6IpamAllocationName); + + if (isL2) { + // Delete the dedicated L2 VPC now that its only vNet is gone + logger.debug("Deleting dedicated L2 VPC {} on Netris", associatedVpc.getName()); + VPCResponseObjectOK deleteResponse = deleteVpcInternal(associatedVpc); + if (deleteResponse == null || !deleteResponse.isIsSuccess()) { + logger.warn("Failed to delete dedicated L2 VPC {} after vNet deletion — manual cleanup may be required", associatedVpc.getName()); + } + } else { + String netrisSubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetCidr); + deleteSubnetInternal(vpcFilter, netrisVnetName, netrisSubnetName); + if (Objects.nonNull(vnetV6Cidr)) { + String netrisV6IpamAllocationName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_ALLOCATION, vnetV6Cidr); + String netrisV6SubnetName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.IPAM_SUBNET, String.valueOf(cmd.getVpcId()), vnetV6Cidr); + deleteSubnetInternal(vpcFilter, netrisVnetName, netrisV6SubnetName); + deleteVpcIpamAllocationInternal(associatedVpc, netrisV6IpamAllocationName); + } } } catch (Exception e) { @@ -1542,6 +1703,18 @@ public boolean createStaticNatRule(CreateOrUpdateNetrisNatCommand cmd) { logger.error("Could not find the Netris VPC resource with name {} and tenant ID {}", netrisVpcName, tenantId); return false; } + + NatGetBody existingRule = netrisNatRuleExists(staticNatRuleName); + if (existingRule != null) { + logger.debug("Static NAT rule '{}' already exists on Netris, skipping creation", staticNatRuleName); + return true; + } + // Backward compatibility: rule with legacy naming convention (no public IP post-fixed) exists - don't create a duplicate + String legacyName = getLegacyStaticNatRuleName(staticNatRuleName); + if (legacyName != null && netrisNatRuleExists(legacyName) != null) { + logger.debug("Legacy static NAT rule '{}' already exists on Netris, skipping creation of '{}'", legacyName, staticNatRuleName); + return true; + } // Create a /32 subnet for the DNAT IP createNatSubnet(cmd, natIP, vpcResource.getId()); NatApi natApi = apiClient.getApiStubForMethod(NatApi.class); @@ -1590,12 +1763,12 @@ private void createNatSubnet(NetrisCommand cmd, String natIp, Integer netrisVpcI IpTreeSubnet existingSubnet = matchedSubnets.stream().filter(x -> x.getPrefix().equals(natIp)).collect(Collectors.toList()).get(0); if (existingSubnet.getPurpose() != IpTreeSubnet.PurposeEnum.NAT) { VPCListing systemVpc = getSystemVpc(); - logger.debug("Subnet: {} already exists, but purpose is not NAT, updating its purpose to 'nat'", natIp); + logger.debug("Subnet: {} already exists with purpose '{}', updating its purpose to 'nat'", natIp, existingSubnet.getPurpose()); updateIpamSubnetInternal(existingSubnet.getId().intValue(), netrisSubnetName, natIp, SubnetBody.PurposeEnum.NAT, systemVpc, null); } } } catch (ApiException e) { - throw new CloudRuntimeException(String.format("Failed to create subnet for %s with NAT purpose", natIp)); + logAndThrowException(String.format("Failed to create subnet for %s with NAT purpose (current purpose conflicts and could not be changed)", natIp), e); } } @@ -1611,12 +1784,13 @@ private void createLBSubnet(NetrisCommand cmd, String lbIp, Integer netrisVpcId) if (matchedSubnets.isEmpty()) { createIpamSubnetInternal(netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null); } else if (IpTreeSubnet.PurposeEnum.LOAD_BALANCER != matchedSubnets.get(0).getPurpose()){ - logger.debug("Updating existing NAT subnet {} to have load balancer purpose", netrisSubnetName); - updateIpamSubnetInternal(matchedSubnets.get(0).getId().intValue(), netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null); + IpTreeSubnet existingSubnet = matchedSubnets.get(0); + logger.debug("Updating existing NAT subnet {} with purpose '{}' to have load balancer purpose", netrisSubnetName, existingSubnet.getPurpose()); + updateIpamSubnetInternal(existingSubnet.getId().intValue(), netrisSubnetName, lbIp, SubnetBody.PurposeEnum.LOAD_BALANCER, systemVpc, null); } logger.debug("LB subnet: {} already exists", netrisSubnetName); } catch (ApiException e) { - throw new CloudRuntimeException(String.format("Failed to create subnet for %s with LB purpose", lbIp)); + logAndThrowException(String.format("Failed to create subnet for %s with LB purpose (current purpose conflicts and could not be changed)", lbIp), e); } } @@ -1813,6 +1987,26 @@ private void deleteSubnetInternal(FilterByVpc vpcFilter, String netrisVnetName, } } + private void rollbackVnetResources(VPCListing associatedVpc, String ipv4SubnetName, String ipv6SubnetName, + String ipv6AllocationName, String networkName) { + logger.warn("Rolling back Netris resources created for network {}", networkName); + FilterByVpc vpcFilter = new FilterByVpc(); + vpcFilter.add(associatedVpc.getId()); + try { + if (ipv6SubnetName != null) { + deleteSubnetInternal(vpcFilter, null, ipv6SubnetName); + } + if (ipv6AllocationName != null) { + deleteVpcIpamAllocationInternal(associatedVpc, ipv6AllocationName); + } + if (ipv4SubnetName != null) { + deleteSubnetInternal(vpcFilter, null, ipv4SubnetName); + } + } catch (Exception e) { + logger.error("Failed to rollback Netris resources for network {} - manual cleanup may be required: {}", networkName, e.getMessage()); + } + } + private SubnetBody getIpamSubnetBody(VPCListing vpc, SubnetBody.PurposeEnum purpose, String subnetName, String subnetPrefix, Boolean isGlobalRouting) { SubnetBody subnetBody = new SubnetBody(); subnetBody.setName(subnetName); @@ -1886,24 +2080,8 @@ VnetResAddBody createVnetInternal(VPCListing associatedVpc, String netrisVnetNam vnetBody.setCustomAnycastMac(""); - VnetAddBodyGateways gatewayV4 = new VnetAddBodyGateways(); - gatewayV4.prefix(netrisGateway); - gatewayV4.setDhcpEnabled(false); - VnetAddBodyDhcp dhcp = new VnetAddBodyDhcp(); - dhcp.setEnd(""); - dhcp.setStart(""); - dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet()); - gatewayV4.setDhcp(dhcp); List gatewaysList = new ArrayList<>(); - gatewaysList.add(gatewayV4); - - if (Objects.nonNull(netrisV6Cidr)) { - VnetAddBodyGateways gatewayV6 = new VnetAddBodyGateways(); - gatewayV6.prefix(NetUtils.getIpv6Gateway(netrisV6Cidr)); - gatewayV6.setDhcpEnabled(false); - gatewayV6.setDhcp(dhcp); - gatewaysList.add(gatewayV6); - } + updateGatewayList(gatewaysList, netrisGateway, netrisV6Cidr); vnetBody.setGateways(gatewaysList); vnetBody.setGuestTenants(new ArrayList<>()); @@ -1948,6 +2126,32 @@ VnetResAddBody createVnetInternal(VPCListing associatedVpc, String netrisVnetNam } } + private void updateGatewayList(List gatewaysList, String netrisGateway, String netrisV6Cidr) { + if (netrisGateway != null) { + VnetAddBodyGateways gatewayV4 = new VnetAddBodyGateways(); + gatewayV4.prefix(netrisGateway); + gatewayV4.setDhcpEnabled(false); + VnetAddBodyDhcp dhcp = new VnetAddBodyDhcp(); + dhcp.setEnd(""); + dhcp.setStart(""); + dhcp.setOptionSet(new VnetAddBodyDhcpOptionSet()); + gatewayV4.setDhcp(dhcp); + gatewaysList.add(gatewayV4); + + if (Objects.nonNull(netrisV6Cidr)) { + VnetAddBodyDhcp dhcpV6 = new VnetAddBodyDhcp(); + dhcpV6.setEnd(""); + dhcpV6.setStart(""); + dhcpV6.setOptionSet(new VnetAddBodyDhcpOptionSet()); + VnetAddBodyGateways gatewayV6 = new VnetAddBodyGateways(); + gatewayV6.prefix(NetUtils.getIpv6Gateway(netrisV6Cidr)); + gatewayV6.setDhcpEnabled(false); + gatewayV6.setDhcp(dhcpV6); + gatewaysList.add(gatewayV6); + } + } + } + private String getNetrisVpcNameSuffix(Long vpcId, String vpcName, Long networkId, String networkName, boolean isVpc) { String suffix = null; if (isVpc) { @@ -1971,12 +2175,25 @@ private NatGetBody netrisNatRuleExists(String netrisNatRule) { return null; } return data.get(0); - } catch (ApiException e) { throw new CloudRuntimeException("Failed to list Netris NAT rules"); } } + /** + * Derives the legacy static NAT rule name (without the public IP suffix) from the new name format. + * New format: ...-VM{vmId}-STATICNAT-{publicIp} + * Legacy format: ...-VM{vmId}-STATICNAT + * Returns null if the name doesn't match the new format. + */ + private String getLegacyStaticNatRuleName(String newName) { + int idx = newName.indexOf("-STATICNAT-"); + if (idx == -1) { + return null; + } + return newName.substring(0, idx + "-STATICNAT".length()); + } + private VPCListing getNetrisVpcResource(String netrisVpcName) { VPCListing vpcResource = getVpcByNameAndTenant(netrisVpcName); if (vpcResource == null) { diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java index 0e798ebb6be1..0e716cc4979a 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisElement.java @@ -199,8 +199,12 @@ protected static Map> initCapab Map sourceNatCapabilities = new HashMap<>(); sourceNatCapabilities.put(Network.Capability.RedundantRouter, "true"); sourceNatCapabilities.put(Network.Capability.SupportedSourceNatTypes, "peraccount"); - capabilities.put(Network.Service.Gateway, null); capabilities.put(Network.Service.SourceNat, sourceNatCapabilities); + + final Map gatewayCapabilities = new HashMap<>(); + gatewayCapabilities.put(Network.Capability.RedundantRouter, "true"); + capabilities.put(Network.Service.Gateway, gatewayCapabilities); + return capabilities; } @@ -360,8 +364,12 @@ public boolean shutdown(Network network, ReservationContext context, boolean cle @Override public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { - Account account = accountManager.getAccount(network.getAccountId()); NetworkVO networkVO = networkDao.findById(network.getId()); + if (Network.GuestType.L2.equals(networkVO.getGuestType())) { + logger.trace("Skipping NetrisElement.destroy() for L2 network {} - handled by guru trash()", network.getId()); + return true; + } + Account account = accountManager.getAccount(network.getAccountId()); DataCenterVO zone = dataCenterDao.findById(network.getDataCenterId()); DomainVO domain = domainDao.findById(account.getDomainId()); if (Objects.isNull(zone)) { @@ -377,7 +385,7 @@ public boolean destroy(Network network, ReservationContext context) throws Concu vpcName = vpc.getName(); } } - netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId, networkVO.getName(), network.getId(), network.getCidr()); + netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId, networkVO.getName(), network.getId(), network.getCidr(), false); return true; } @@ -622,7 +630,7 @@ private boolean applyPFRulesInternal(Network network, List r .setNetworkResourceName(netrisObject.getNetworkResourceName()) .setVpcResource(netrisObject.isVpcResource()) .setVmId(Objects.nonNull(vm) ? vm.getId() : 0) - .setVmIp(Objects.nonNull(vm) ? vm.getPrivateIpAddress() : null) + .setVmIp(rule.getDestinationIpAddress().addr()) .setPublicIp(publicIp.getAddress().addr()) .setPrivatePort(privatePort) .setPublicPort(publicPort) diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java index df566e90b67d..de745626cea8 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisGuestNetworkGuru.java @@ -17,6 +17,7 @@ package org.apache.cloudstack.service; import com.cloud.dc.DataCenter; +import com.cloud.dc.DataCenterVO; import com.cloud.deploy.DeployDestination; import com.cloud.deploy.DeploymentPlan; import com.cloud.domain.DomainVO; @@ -44,6 +45,7 @@ import com.cloud.user.Account; import com.cloud.utils.db.DB; import com.cloud.utils.exception.CloudRuntimeException; +import com.cloud.utils.net.NetUtils; import com.cloud.vm.NicProfile; import com.cloud.vm.ReservationContext; import com.cloud.vm.VirtualMachine; @@ -51,6 +53,7 @@ import org.apache.cloudstack.api.ApiCommandResourceType; import org.apache.cloudstack.api.ApiConstants; import org.apache.cloudstack.context.CallContext; +import org.apache.cloudstack.resource.NetrisResourceObjectUtils; import javax.inject.Inject; import java.util.Objects; @@ -75,10 +78,12 @@ public NetrisGuestNetworkGuru() { @Override public boolean canHandle(NetworkOffering offering, DataCenter.NetworkType networkType, PhysicalNetwork physicalNetwork) { + NetworkOffering.NetworkMode networkMode = NetrisResourceObjectUtils.getNetworkMode(offering.getNetworkMode()); return networkType == DataCenter.NetworkType.Advanced && isMyTrafficType(offering.getTrafficType()) - && isMyIsolationMethod(physicalNetwork) && (NetworkOffering.NetworkMode.ROUTED.equals(offering.getNetworkMode()) + && isMyIsolationMethod(physicalNetwork) && (NetworkOffering.NetworkMode.ROUTED.equals(networkMode) || (networkOfferingServiceMapDao.isProviderForNetworkOffering( - offering.getId(), Network.Provider.Netris) && NetworkOffering.NetworkMode.NATTED.equals(offering.getNetworkMode()))); + offering.getId(), Network.Provider.Netris) && NetworkOffering.NetworkMode.NATTED.equals(networkMode)) + || (Network.GuestType.L2.equals(offering.getGuestType()))); } @Override @@ -174,8 +179,10 @@ public boolean update(Network network, String prevNetworkName) { vpcName = vpc.getName(); } } + NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(network.getNetworkOfferingId()); + boolean isL2 = networkOfferingVO != null && Network.GuestType.L2.equals(networkOfferingVO.getGuestType()); return netrisService.updateVnetResource(network.getDataCenterId(), network.getAccountId(), network.getDomainId(), - vpcName, vpcId, network.getName(), network.getId(), prevNetworkName); + vpcName, vpcId, network.getName(), network.getId(), prevNetworkName, isL2); } @Override @@ -201,7 +208,8 @@ public Network implement(Network network, NetworkOffering offering, DeployDestin if (network.getName() != null) { implemented.setName(network.getName()); } - allocateVnet(network, implemented, network.getDataCenterId(), network.getPhysicalNetworkId(), network.getReservationId()); + + allocateVnet(network, implemented, network.getDataCenterId(), network.getPhysicalNetworkId(), context != null ? context.getReservationId() : null); return implemented; } @@ -210,12 +218,13 @@ protected void allocateVnet(Network network, NetworkVO implemented, long dcId, l throws InsufficientVirtualNetworkCapacityException { String vnet = null; Long networkId = implemented.getId() > 0 ? implemented.getId() : network.getId(); + long accountId = implemented.getAccountId() > 0 ? implemented.getAccountId() : network.getAccountId(); if (network.getBroadcastUri() == null) { NetworkDetailVO netrisVnetDetail = networkDetailsDao.findDetail(networkId, ApiConstants.NETRIS_VXLAN_ID); if (nonNull(netrisVnetDetail)) { vnet = netrisVnetDetail.getValue(); } else { - vnet = _dcDao.allocateVnet(dcId, physicalNetworkId, network.getAccountId(), reservationId, UseSystemGuestVlans.valueIn(network.getAccountId())); + vnet = _dcDao.allocateVnet(dcId, physicalNetworkId, accountId, reservationId, UseSystemGuestVlans.valueIn(accountId)); if (vnet == null) { throw new InsufficientVirtualNetworkCapacityException("Unable to allocate vnet as a " + "part of network " + network + " implement ", DataCenter.class, dcId); @@ -223,7 +232,7 @@ protected void allocateVnet(Network network, NetworkVO implemented, long dcId, l networkDetailsDao.addDetail(networkId, ApiConstants.NETRIS_VXLAN_ID, vnet, true); } implemented.setBroadcastUri(Networks.BroadcastDomainType.Netris.toUri(vnet)); - ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), network.getAccountId(), EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_ASSIGN, + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), accountId, EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_ASSIGN, "Assigned Zone vNet: " + vnet + " Network Id: " + networkId, networkId, ApiCommandResourceType.Network.toString(), 0); } else { implemented.setBroadcastUri(network.getBroadcastUri()); @@ -264,8 +273,8 @@ public NicProfile allocate(Network network, NicProfile nic, VirtualMachineProfil } NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(network.getNetworkOfferingId()); - - if (isNull(network.getVpcId()) && networkOfferingVO.getNetworkMode().equals(NetworkOffering.NetworkMode.NATTED)) { + NetworkOffering.NetworkMode networkMode = NetrisResourceObjectUtils.getNetworkMode(networkOfferingVO.getNetworkMode()); + if (isNull(network.getVpcId()) && NetworkOffering.NetworkMode.NATTED.equals(networkMode)) { // Netris Natted mode long domainId = domain.getId(); long accountId = account.getId(); @@ -299,6 +308,80 @@ public void commitMigration(NicProfile nic, Network network, VirtualMachineProfi // Do nothing } + private void releaseAllocatedVnet(Network network) { + String vxlanId = null; + if (Objects.nonNull(network.getBroadcastUri())) { + vxlanId = Networks.BroadcastDomainType.getValue(network.getBroadcastUri()); + } else { + NetworkDetailVO vxlanDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID); + if (Objects.nonNull(vxlanDetail)) { + vxlanId = vxlanDetail.getValue(); + } + } + + if (Objects.isNull(vxlanId)) { + logger.debug("No VXLAN allocated for Netris network {}, nothing to release", network.getId()); + return; + } + + logger.debug("Releasing VXLAN {} for Netris network {}", vxlanId, network.getId()); + _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(), + network.getAccountId(), network.getReservationId()); + // Attempt to clean up with null reservation ID + _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(), + network.getAccountId(), null); + if (network.getAccountId() != 0) { + _dcDao.releaseVnet(vxlanId, network.getDataCenterId(), network.getPhysicalNetworkId(), + 0L, null); + } + ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), network.getAccountId(), + EventVO.LEVEL_INFO, EventTypes.EVENT_ZONE_VXLAN_RELEASE, + "Released Zone vNet: " + vxlanId + " for Network: " + network.getId(), + network.getDataCenterId(), ApiCommandResourceType.Zone.toString(), 0); + + NetworkDetailVO vxlanDetail = networkDetailsDao.findDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID); + if (Objects.nonNull(vxlanDetail)) { + networkDetailsDao.removeDetail(network.getId(), ApiConstants.NETRIS_VXLAN_ID); + } + } + + @Override + public boolean trash(Network network, NetworkOffering offering) { + if (Network.GuestType.L2.equals(offering.getGuestType())) { + NetworkVO networkVO = _networkDao.findById(network.getId()); + if (networkVO == null) { + logger.warn("Could not find network {} to trash, skipping Netris L2 cleanup", network.getId()); + return super.trash(network, offering); + } + + DataCenterVO zone = _dcDao.findById(network.getDataCenterId()); + if (Objects.isNull(zone)) { + logger.error("Cannot find zone with ID {} while trashing L2 Netris network {}", network.getDataCenterId(), network.getId()); + return false; + } + + Account account = accountDao.findById(network.getAccountId()); + if (Objects.isNull(account)) { + logger.error("Cannot find account with ID {} while trashing L2 Netris network {}", network.getAccountId(), network.getId()); + return false; + } + + DomainVO domain = domainDao.findById(account.getDomainId()); + if (Objects.isNull(domain)) { + logger.error("Cannot find domain with ID {} while trashing L2 Netris network {}", account.getDomainId(), network.getId()); + return false; + } + + logger.debug("Trashing L2 Netris network {}, deleting Netris vNet resource", network.getId()); + netrisService.deleteVnetResource(zone.getId(), account.getId(), domain.getId(), + null, null, networkVO.getName(), network.getId(), network.getCidr(), true); + } + + releaseAllocatedVnet(network); + + return super.trash(network, offering); + } + public void createNetrisVnet(NetworkVO networkVO, DataCenter zone) { Account account = accountDao.findById(networkVO.getAccountId()); if (isNull(account)) { @@ -312,33 +395,38 @@ public void createNetrisVnet(NetworkVO networkVO, DataCenter zone) { } String vpcName = null; Long vpcId = null; - Boolean globalRouting = null; + boolean isL2 = false; long networkOfferingId = networkVO.getNetworkOfferingId(); + NetworkOfferingVO networkOfferingVO = networkOfferingDao.findById(networkOfferingId); - if (NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode())) { - globalRouting = true; + NetUtils.InternetProtocol internetProtocol = networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingVO.getId()); + NetworkOffering.NetworkMode networkMode = networkOfferingVO.getNetworkMode(); + if (Network.GuestType.L2.equals(networkOfferingVO.getGuestType())) { + isL2 = true; } - if (nonNull(networkVO.getVpcId())) { - VpcVO vpc = _vpcDao.findById(networkVO.getVpcId()); - if (isNull(vpc)) { - throw new CloudRuntimeException(String.format("Failed to find VPC network with id: %s", networkVO.getVpcId())); - } - vpcName = vpc.getName(); - vpcId = vpc.getId(); - } else { - logger.debug(String.format("Creating IPAM Allocation before creating IPAM Subnet", networkVO.getName())); - boolean isSourceNatSupported = !NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode()) && - networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(networkVO.getNetworkOfferingId(), Network.Service.SourceNat); - boolean result = netrisService.createVpcResource(zone.getId(), networkVO.getAccountId(), networkVO.getDomainId(), - networkVO.getId(), networkVO.getName(), isSourceNatSupported, networkVO.getCidr(), false); - if (!result) { - String msg = String.format("Error creating Netris VPC for the network: %s", networkVO.getName()); - logger.error(msg); - throw new CloudRuntimeException(msg); + if (!isL2) { + if (nonNull(networkVO.getVpcId())) { + VpcVO vpc = _vpcDao.findById(networkVO.getVpcId()); + if (isNull(vpc)) { + throw new CloudRuntimeException(String.format("Failed to find VPC network with id: %s", networkVO.getVpcId())); + } + vpcName = vpc.getName(); + vpcId = vpc.getId(); + } else { + logger.debug(String.format("Creating IPAM Allocation before creating IPAM Subnet", networkVO.getName())); + boolean isSourceNatSupported = !NetworkOffering.NetworkMode.ROUTED.equals(networkOfferingVO.getNetworkMode()) && + networkOfferingServiceMapDao.areServicesSupportedByNetworkOffering(networkVO.getNetworkOfferingId(), Network.Service.SourceNat); + boolean result = netrisService.createVpcResource(zone.getId(), networkVO.getAccountId(), networkVO.getDomainId(), + networkVO.getId(), networkVO.getName(), isSourceNatSupported, networkVO.getCidr(), false); + if (!result) { + String msg = String.format("Error creating Netris VPC for the network: %s", networkVO.getName()); + logger.error(msg); + throw new CloudRuntimeException(msg); + } } } boolean result = netrisService.createVnetResource(zone.getId(), account.getId(), domain.getId(), vpcName, vpcId, - networkVO.getName(), networkVO.getId(), networkVO.getCidr(), globalRouting); + networkVO.getName(), networkVO.getId(), networkVO.getCidr(), networkMode, internetProtocol, isL2); if (!result) { throw new CloudRuntimeException("Failed to create Netris vNet resource for network: " + networkVO.getName()); } diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java index f1cc49b71257..7a89412fa07e 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisPublicNetworkGuru.java @@ -27,7 +27,6 @@ import com.cloud.network.dao.NetworkVO; import com.cloud.network.guru.PublicNetworkGuru; import com.cloud.network.netris.NetrisService; -import com.cloud.network.vpc.VpcOffering; import com.cloud.network.vpc.VpcOfferingVO; import com.cloud.network.vpc.VpcVO; import com.cloud.offering.NetworkOffering; @@ -36,6 +35,7 @@ import com.cloud.vm.NicProfile; import com.cloud.vm.VirtualMachineProfile; import org.apache.cloudstack.api.ApiConstants; +import org.apache.cloudstack.resource.NetrisResourceObjectUtils; import org.apache.commons.collections.CollectionUtils; import javax.inject.Inject; @@ -110,7 +110,8 @@ public NicProfile allocate(Network network, NicProfile nic, VirtualMachineProfil Network.Service[] services = { Network.Service.SourceNat }; long networkOfferingId = vpc.getVpcOfferingId(); VpcOfferingVO vpcVO = vpcOfferingDao.findById(networkOfferingId); - boolean sourceNatEnabled = !NetworkOffering.NetworkMode.ROUTED.equals(vpcVO.getNetworkMode()) && + NetworkOffering.NetworkMode vpcNetworkMode = NetrisResourceObjectUtils.getNetworkMode(vpcVO.getNetworkMode()); + boolean sourceNatEnabled = !NetworkOffering.NetworkMode.ROUTED.equals(vpcNetworkMode) && vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), services); logger.info(String.format("Creating Netris VPC %s", vpc.getName())); @@ -121,9 +122,7 @@ public NicProfile allocate(Network network, NicProfile nic, VirtualMachineProfil throw new CloudRuntimeException(msg); } - boolean hasNatSupport = false; - VpcOffering vpcOffering = vpcOfferingDao.findById(vpc.getVpcOfferingId()); - hasNatSupport = NetworkOffering.NetworkMode.NATTED.equals(vpcOffering.getNetworkMode()); + boolean hasNatSupport = NetworkOffering.NetworkMode.NATTED.equals(NetrisResourceObjectUtils.getNetworkMode(vpcVO.getNetworkMode())); if (!hasNatSupport) { return nic; diff --git a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java index 6cd944c46780..5329a199d31c 100644 --- a/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java +++ b/plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisServiceImpl.java @@ -38,6 +38,7 @@ import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.offering.NetworkOffering; import io.netris.model.NatPostBody; import org.apache.cloudstack.agent.api.CreateOrUpdateNetrisACLCommand; import com.cloud.utils.Pair; @@ -222,14 +223,16 @@ public boolean deleteVpcResource(long zoneId, long accountId, long domainId, Vpc } @Override - public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting) { + public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2) { NetworkVO network = networkDao.findById(networkId); String vxlanId = Networks.BroadcastDomainType.getValue(network.getBroadcastUri()); CreateNetrisVnetCommand cmd = new CreateNetrisVnetCommand(zoneId, accountId, domainId, vpcName, vpcId, networkName, networkId, cidr, network.getGateway(), !Objects.isNull(vpcId)); cmd.setVxlanId(Integer.parseInt(vxlanId)); + cmd.setL2(isL2); NetrisProviderVO netrisProvider = netrisProviderDao.findByZoneId(zoneId); cmd.setNetrisTag(netrisProvider.getNetrisTag()); - cmd.setGlobalRouting(globalRouting); + cmd.setNetworkMode(networkMode); + cmd.setInternetProtocol(internetProtocol); if (Objects.nonNull(networkId)) { Ipv6GuestPrefixSubnetNetworkMapVO ipv6PrefixNetworkMapVO = ipv6PrefixNetworkMapDao.findByNetworkId(networkId); if (Objects.nonNull(ipv6PrefixNetworkMapVO)) { @@ -241,18 +244,20 @@ public boolean createVnetResource(Long zoneId, long accountId, long domainId, St } @Override - public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName) { + public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2) { UpdateNetrisVnetCommand cmd = new UpdateNetrisVnetCommand(zoneId, accountId, domainId, networkName, networkId, Objects.nonNull(vpcId)); cmd.setPrevNetworkName(prevNetworkName); cmd.setVpcId(vpcId); cmd.setVpcName(vpcName); + cmd.setL2(isL2); NetrisAnswer answer = sendNetrisCommand(cmd, zoneId); return answer.getResult(); } @Override - public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr) { + public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2) { DeleteNetrisVnetCommand cmd = new DeleteNetrisVnetCommand(zoneId, accountId, domainId, networkName, networkId, vpcName, vpcId, cidr, Objects.nonNull(vpcName)); + cmd.setL2(isL2); Ipv6GuestPrefixSubnetNetworkMapVO ipv6PrefixNetworkMapVO = ipv6PrefixNetworkMapDao.findByNetworkId(networkId); if (Objects.nonNull(ipv6PrefixNetworkMapVO)) { cmd.setvNetV6Cidr(ipv6PrefixNetworkMapVO.getSubnet()); @@ -354,7 +359,7 @@ public boolean createStaticNatRule(long zoneId, long accountId, long domainId, S cmd.setNatRuleType("STATICNAT"); cmd.setNatIp(staticNatIp); cmd.setVmIp(vmIp); - String[] suffixes = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId); + String[] suffixes = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId, staticNatIp); String dnatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes); cmd.setNatRuleName(dnatRuleName); NetrisAnswer answer = sendNetrisCommand(cmd, zoneId); @@ -375,7 +380,7 @@ public boolean deleteStaticNatRule(long zoneId, long accountId, long domainId, S networkId = networkResourceId; } DeleteNetrisNatRuleCommand cmd = new DeleteNetrisNatRuleCommand(zoneId, accountId, domainId, vpcName, vpcId, networkName, networkId, isForVpc); - String suffixes[] = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId); + String suffixes[] = getStaticNatResourceSuffixes(vpcId, networkId, isForVpc, vmId, staticNatIp); String dnatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes); cmd.setNatRuleName(dnatRuleName); cmd.setNatRuleType("STATICNAT"); @@ -547,10 +552,9 @@ public boolean deleteLbRule(NetrisNetworkRule rule) { return answer.getResult(); } - public static String[] getStaticNatResourceSuffixes(Long vpcId, Long networkId, boolean isForVpc, long vmId) { - String[] suffixes = new String[2]; - suffixes[0] = isForVpc ? String.valueOf(vpcId) : String.valueOf(networkId); - suffixes[1] = String.valueOf(vmId); - return suffixes; + public static String[] getStaticNatResourceSuffixes(Long vpcId, Long networkId, boolean isForVpc, long vmId, String publicIp) { + String resourceId = isForVpc ? String.valueOf(vpcId) : String.valueOf(networkId); + // Always include the public IP to ensure uniqueness across multiple static NATs on the same VM + return new String[]{resourceId, String.valueOf(vmId), publicIp}; } } diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java index cc367d181e3f..66fb77d322e5 100644 --- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java +++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/agent/api/CreateNetrisVnetCommandTest.java @@ -16,6 +16,8 @@ // under the License. package org.apache.cloudstack.agent.api; +import com.cloud.offering.NetworkOffering; +import com.cloud.utils.net.NetUtils; import org.junit.Assert; import org.junit.Test; @@ -34,7 +36,8 @@ public class CreateNetrisVnetCommandTest { private static final Integer VXLAN_ID = 100; private static final String NETRIS_TAG = "test-tag"; private static final String IPV6_CIDR = "2001:db8::/64"; - private static final Boolean GLOBAL_ROUTING = true; + private static final NetworkOffering.NetworkMode NETWORK_MODE = NetworkOffering.NetworkMode.NATTED; + private static final NetUtils.InternetProtocol INTERNET_PROTOCOL = NetUtils.InternetProtocol.IPv4; @Test public void testConstructorAndGetters() { @@ -65,13 +68,15 @@ public void testSetters() { command.setVxlanId(VXLAN_ID); command.setNetrisTag(NETRIS_TAG); command.setIpv6Cidr(IPV6_CIDR); - command.setGlobalRouting(GLOBAL_ROUTING); + command.setInternetProtocol(NetUtils.InternetProtocol.IPv4); + command.setNetworkMode(NetworkOffering.NetworkMode.NATTED); // Assert Assert.assertEquals(VXLAN_ID, command.getVxlanId()); Assert.assertEquals(NETRIS_TAG, command.getNetrisTag()); Assert.assertEquals(IPV6_CIDR, command.getIpv6Cidr()); - Assert.assertEquals(GLOBAL_ROUTING, command.isGlobalRouting()); + Assert.assertEquals(NETWORK_MODE, command.getNetworkMode()); + Assert.assertEquals(INTERNET_PROTOCOL, command.getInternetProtocol()); } @Test diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java index 44bd9824b2d2..7f0ceb596bf5 100644 --- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java +++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/resource/NetrisResourceObjectUtilsTest.java @@ -100,10 +100,12 @@ public void testSourceNatName() { @Test public void testStaticNatName() { long vmId = 1234L; + String publicIp = "203.0.113.5"; CreateOrUpdateNetrisNatCommand cmd = new CreateOrUpdateNetrisNatCommand(zoneId, accountId, domainId, vpcName, vpcId, null, null, true, vpcCidr); - String[] suffixes = NetrisServiceImpl.getStaticNatResourceSuffixes(vpcId, null, true, vmId); + String[] suffixes = NetrisServiceImpl.getStaticNatResourceSuffixes(vpcId, null, true, vmId, publicIp); String staticNatRuleName = NetrisResourceObjectUtils.retrieveNetrisResourceObjectName(cmd, NetrisResourceObjectUtils.NetrisObjectType.STATICNAT, suffixes); - String expectedName = String.format("D%s-A%s-Z%s-V%s-VM%s-STATICNAT", domainId, accountId, zoneId, vpcId, vmId); + // Public IP always appended for uniqueness; legacy rules without IP are handled via fallback on delete + String expectedName = String.format("D%s-A%s-Z%s-V%s-VM%s-STATICNAT-%s", domainId, accountId, zoneId, vpcId, vmId, publicIp); Assert.assertEquals(expectedName, staticNatRuleName); } } diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java index a2622549deb7..1010cd48114f 100644 --- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java +++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisElementTest.java @@ -136,6 +136,6 @@ public void testDeleteNetwork() throws ResourceUnavailableException { ReservationContext context = Mockito.mock(ReservationContext.class); netrisElement.destroy(network, context); Mockito.verify(netrisService).deleteVnetResource(zoneId, accountId, domainId, vpcName, vpcId, - networkName, networkId, networkCidr); + networkName, networkId, networkCidr, false); } } diff --git a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java index ad8d46c0dd89..d228ca9217c1 100644 --- a/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java +++ b/plugins/network-elements/netris/src/test/java/org/apache/cloudstack/service/NetrisGuestNetworkGuruTest.java @@ -41,6 +41,7 @@ import com.cloud.offerings.dao.NetworkOfferingServiceMapDao; import com.cloud.user.AccountVO; import com.cloud.user.dao.AccountDao; +import com.cloud.utils.net.NetUtils; import com.cloud.vm.ReservationContext; import org.junit.After; import org.junit.Assert; @@ -206,12 +207,49 @@ public void testDesignNetrisNetwork() { @Test public void testCreateNetrisVnetVpcNetworkRoutedMode() { Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.ROUTED); + Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.IPv4); Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), - Mockito.anyBoolean())).thenReturn(true); + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true); guru.createNetrisVnet(network, zone); Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId, - networkName, networkId, networkCidr, true); + networkName, networkId, networkCidr, NetworkOffering.NetworkMode.ROUTED, NetUtils.InternetProtocol.IPv4, false); + } + + @Test + public void testCreateNetrisVnetVpcNetworkNattedMode() { + Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED); + Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.IPv4); + Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true); + guru.createNetrisVnet(network, zone); + Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId, + networkName, networkId, networkCidr, NetworkOffering.NetworkMode.NATTED, NetUtils.InternetProtocol.IPv4, false); + } + + @Test + public void testCreateNetrisVnetVpcNetworkDualStackNattedMode() { + Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.NATTED); + Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.DualStack); + Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true); + guru.createNetrisVnet(network, zone); + Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId, + networkName, networkId, networkCidr, NetworkOffering.NetworkMode.NATTED, NetUtils.InternetProtocol.DualStack, false); + } + + @Test + public void testCreateNetrisVnetVpcNetworkDualStackRoutedMode() { + Mockito.when(networkOffering.getNetworkMode()).thenReturn(NetworkOffering.NetworkMode.ROUTED); + Mockito.when(networkOfferingDao.getNetworkOfferingInternetProtocol(networkOfferingId)).thenReturn(NetUtils.InternetProtocol.DualStack); + Mockito.when(netrisService.createVnetResource(Mockito.anyLong(), Mockito.anyLong(), Mockito.anyLong(), + Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyString(), + Mockito.any(), Mockito.any(), Mockito.anyBoolean())).thenReturn(true); + guru.createNetrisVnet(network, zone); + Mockito.verify(netrisService).createVnetResource(zoneId, accountId, domainId, vpcName, vpcId, + networkName, networkId, networkCidr, NetworkOffering.NetworkMode.ROUTED, NetUtils.InternetProtocol.DualStack, false); } @Test diff --git a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java index 7ea4b7d5834f..8f6f6199085c 100644 --- a/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java +++ b/server/src/main/java/com/cloud/api/query/dao/VpcOfferingJoinDaoImpl.java @@ -71,6 +71,7 @@ public VpcOfferingResponse newVpcOfferingResponse(VpcOffering offering) { offeringResponse.setSupportsDistributedRouter(offering.isSupportsDistributedRouter()); offeringResponse.setSupportsRegionLevelVpc(offering.isOffersRegionLevelVPC()); offeringResponse.setCreated(offering.getCreated()); + offeringResponse.setRedundantRouter(offering.isRedundantRouter()); if (offering.getRoutingMode() != null) { offeringResponse.setRoutingMode(offering.getRoutingMode().toString()); } diff --git a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java index 6a4f3745f1b1..2d5102d8848e 100644 --- a/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java +++ b/server/src/main/java/com/cloud/network/IpAddressManagerImpl.java @@ -1417,6 +1417,31 @@ private void checkPublicIpOnExternalProviderZone(DataCenter zone, String ip) { } } + /** + * When the zone is linked to external provider Netris and no explicit IP was requested: + * restrict the candidate pool to the VLANs reserved for Netris, so that generic acquisitions + * (e.g. associateIpAddress called by the CloudStack cloud-controller-manager on behalf of a + * Kubernetes Service of type LoadBalancer) don't fall back to the VR's public range. + * Returns null when the zone has no Netris provider, meaning no restriction is applied. + * + * returns VLAN IDS tagged with forNetris = true + */ + private List getNetrisVlanDbIds(DataCenter zone) { + long zoneId = zone.getId(); + NetrisProviderVO netrisProvider = netrisProviderDao.findByZoneId(zoneId); + if (netrisProvider == null) { + return null; + } + List vlanIds = _vlanDao.listVlansForExternalNetworkProvider(zoneId, ApiConstants.NETRIS_DETAIL_KEY).stream() + .map(VlanVO::getId).collect(Collectors.toList()); + if (vlanIds.isEmpty()) { + throw new CloudRuntimeException(String.format( + "Cannot allocate public IP in zone %s: no VLANs tagged '%s=true' found for the external provider", + zone.getName(), ApiConstants.NETRIS_DETAIL_KEY)); + } + return vlanIds; + } + @DB @Override public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Account caller, User callerUser, final DataCenter zone, final Boolean displayIp, final String ipaddress) @@ -1425,8 +1450,6 @@ public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Accou final VlanType vlanType = VlanType.VirtualNetwork; final boolean assign = false; - checkPublicIpOnExternalProviderZone(zone, ipaddress); - if (Grouping.AllocationState.Disabled == zone.getAllocationState() && !_accountMgr.isRootAdmin(caller.getId())) { // zone is of type DataCenter. See DataCenterVO.java. PermissionDeniedException ex = new PermissionDeniedException(generateErrorMessageForOperationOnDisabledZone("allocate IP addresses", zone)); @@ -1434,6 +1457,12 @@ public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Accou throw ex; } + checkPublicIpOnExternalProviderZone(zone, ipaddress); + + // Only steer the range when no explicit IP was requested: an explicit ipaddress is already + // validated against the provider's pool above by checkPublicIpOnExternalProviderZone. + final List vlanDbIds = ipaddress == null ? getNetrisVlanDbIds(zone) : null; + PublicIp ip = null; Account accountToLock = null; @@ -1461,7 +1490,7 @@ public IpAddress allocateIp(final Account ipOwner, final boolean isSystem, Accou ip = Transaction.execute(new TransactionCallbackWithException() { @Override public PublicIp doInTransaction(TransactionStatus status) throws InsufficientAddressCapacityException { - PublicIp ip = fetchNewPublicIp(zone.getId(), null, null, ipOwner, vlanType, null, false, assign, ipaddress, null, isSystem, null, displayIp, false); + PublicIp ip = fetchNewPublicIp(zone.getId(), null, vlanDbIds, ipOwner, vlanType, null, false, assign, ipaddress, null, isSystem, null, displayIp, false); if (ip == null) { InsufficientAddressCapacityException ex = new InsufficientAddressCapacityException("Unable to find available public IP addresses", DataCenter.class, zone diff --git a/server/src/main/java/com/cloud/network/NetworkModelImpl.java b/server/src/main/java/com/cloud/network/NetworkModelImpl.java index 07305c0e6418..0e53c9a6bfbe 100644 --- a/server/src/main/java/com/cloud/network/NetworkModelImpl.java +++ b/server/src/main/java/com/cloud/network/NetworkModelImpl.java @@ -495,7 +495,8 @@ Map> getProviderServicesMap(long networkId) { public Map> getProviderToIpList(Network network, Map> ipToServices) { NetworkOffering offering = _networkOfferingDao.findById(network.getNetworkOfferingId()); boolean isForNsx = isProviderForNetworkOffering(Provider.Nsx, offering.getId()); - if (!offering.isConserveMode() && !isForNsx) { + boolean isForNetris = isProviderForNetworkOffering(Provider.Netris, offering.getId()); + if (!offering.isConserveMode() && !isForNsx && !isForNetris) { for (PublicIpAddress ip : ipToServices.keySet()) { Set services = new HashSet(); services.addAll(ipToServices.get(ip)); @@ -1649,7 +1650,8 @@ public boolean checkIpForService(IpAddress userIp, Service service, Long network return false; } boolean isForNsx = isProviderForNetworkOffering(Provider.Nsx, offering.getId()); - if (!offering.isConserveMode() && !isForNsx) { + boolean isForNetris = isProviderForNetworkOffering(Provider.Netris, offering.getId()); + if (!offering.isConserveMode() && !isForNsx && !isForNetris) { return canIpUsedForNonConserveService(publicIp, service); } return true; diff --git a/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java b/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java index 3c677cb19e24..76a8052bff9d 100644 --- a/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java +++ b/server/src/main/java/com/cloud/network/guru/GuestNetworkGuru.java @@ -17,7 +17,6 @@ package com.cloud.network.guru; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Random; @@ -529,16 +528,15 @@ public void shutdown(final NetworkProfile profile, final NetworkOffering offerin return; // Nothing to do here if the uri is null already } - if ((profile.getBroadcastDomainType() == BroadcastDomainType.Vlan || profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan || profile.getBroadcastDomainType() == BroadcastDomainType.Netris) && !offering.isSpecifyVlan()) { - logger.debug("Releasing vnet for the network: {}", profile); - _dcDao.releaseVnet(BroadcastDomainType.getValue(profile.getBroadcastUri()), profile.getDataCenterId(), profile.getPhysicalNetworkId(), profile.getAccountId(), + if ((profile.getBroadcastDomainType() == BroadcastDomainType.Vlan || profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan) && !offering.isSpecifyVlan()) { + logger.debug("Releasing vnet for the network id=" + profile.getId()); + final String vnetValue = BroadcastDomainType.getValue(profile.getBroadcastUri()); + _dcDao.releaseVnet(vnetValue, profile.getDataCenterId(), profile.getPhysicalNetworkId(), profile.getAccountId(), profile.getReservationId()); - String eventType = EventTypes.EVENT_ZONE_VLAN_RELEASE; - if (Arrays.asList(BroadcastDomainType.Vxlan, BroadcastDomainType.Netris).contains(profile.getBroadcastDomainType())) { - eventType = EventTypes.EVENT_ZONE_VXLAN_RELEASE; - } + String eventType = profile.getBroadcastDomainType() == BroadcastDomainType.Vxlan + ? EventTypes.EVENT_ZONE_VXLAN_RELEASE : EventTypes.EVENT_ZONE_VLAN_RELEASE; ActionEventUtils.onCompletedActionEvent(CallContext.current().getCallingUserId(), profile.getAccountId(), EventVO.LEVEL_INFO, eventType, - "Released Zone Vnet: " + BroadcastDomainType.getValue(profile.getBroadcastUri()) + " for Network: " + profile.getId(), + "Released Zone Vnet: " + vnetValue + " for Network: " + profile.getId(), profile.getDataCenterId(), ApiCommandResourceType.Zone.toString(), 0); } 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 5717f8745aca..ffcd1fd00d3b 100644 --- a/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java @@ -714,10 +714,11 @@ public VpcOffering createVpcOffering(final String name, final String displayText Service redundantRouterService = Service.SourceNat; if (CollectionUtils.isNotEmpty(sourceNatServiceProviders)) { svcProviderMap.put(Service.Gateway, sourceNatServiceProviders); - } else if (NetworkOffering.NetworkMode.ROUTED.equals(networkMode) && org.apache.commons.lang3.StringUtils.isBlank(externalProvider)) { - // For Routed mode, add the Gateway service except for external providers such as NSX, Netris to not override the svcProviderMap mapping - svcProviderMap.put(Service.Gateway, Sets.newHashSet(Provider.VPCVirtualRouter)); + } else if (NetworkOffering.NetworkMode.ROUTED.equals(networkMode)) { redundantRouterService = Service.Gateway; + if (org.apache.commons.lang3.StringUtils.isBlank(externalProvider)) { + svcProviderMap.put(Service.Gateway, Sets.newHashSet(Provider.VPCVirtualRouter)); + } } validateConnectivtyServiceCapabilities(svcProviderMap.get(Service.Connectivity), serviceCapabilityList); @@ -3797,7 +3798,7 @@ public IPAddressVO getIpAddressForVpcVr(Vpc vpc, IPAddressVO ipAddress, boolean return ipAddressForVR; } else if (ipAddress != null) { if (ipAddress.isSourceNat()) { - throw new InvalidParameterValueException("Vpn service can not be configured on the Source NAT IP of VPC id=" + ipAddress.getVpcId()); + throw new InvalidParameterValueException(String.format("Vpn service can not be configured on the Source NAT IP of VPC id = %s. Please acquire another IP to setup VPN", ipAddress.getVpcId())); } return ipAddress; } diff --git a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java index 24c2f2221d96..5e5261f657b6 100644 --- a/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpn/RemoteAccessVpnManagerImpl.java @@ -69,6 +69,7 @@ import com.cloud.network.vpc.Vpc; import com.cloud.network.vpc.VpcManager; import com.cloud.network.vpc.dao.VpcDao; +import com.cloud.network.vpc.dao.VpcOfferingServiceMapDao; import com.cloud.projects.Project.ListProjectResourcesCriteria; import com.cloud.server.ConfigurationServer; import com.cloud.user.Account; @@ -134,6 +135,8 @@ public class RemoteAccessVpnManagerImpl extends ManagerBase implements RemoteAcc ConfigurationServer _configServer; @Inject VpcDao _vpcDao; + @Inject + VpcOfferingServiceMapDao _vpcOfferingServiceMapDao; int _userLimit; int _pskLength; @@ -243,6 +246,9 @@ public RemoteAccessVpn createRemoteAccessVpn(final long publicIpId, String ipRan validateIpAddressForVpnServiceOnNetwork(network, ipAddress); } else { Vpc vpc = _vpcDao.findById(vpcId); + if (!_vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), new Network.Service[]{Network.Service.Vpn})) { + throw new InvalidParameterValueException(String.format("Vpn service is not supported for VPC: %s (%s) ", vpc.getName(), vpc.getUuid())); + } cidr = NetUtils.getCidr(vpc.getCidr()); validateIpAddressForVpnServiceOnVpc(vpc, ipAddress); } diff --git a/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java b/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java index ad1d1f02682e..34e2cf712492 100644 --- a/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java +++ b/server/src/main/java/com/cloud/network/vpn/Site2SiteVpnManagerImpl.java @@ -144,6 +144,9 @@ public Site2SiteVpnGateway createVpnGateway(CreateVpnGatewayCmd cmd) { if (vpc == null) { throw new InvalidParameterValueException("Invalid VPC " + vpcId + " for site to site vpn gateway creation!"); } + if (!vpcOfferingServiceMapDao.areServicesSupportedByVpcOffering(vpc.getVpcOfferingId(), new Network.Service[]{Network.Service.Vpn})) { + throw new InvalidParameterValueException(String.format("Vpn service is not supported for VPC: %s (%s) ", vpc.getName(), vpc.getUuid())); + } Site2SiteVpnGatewayVO gws = _vpnGatewayDao.findByVpcId(vpcId); if (gws != null) { throw new InvalidParameterValueException(String.format("The VPN gateway of VPC %s already existed!", vpc)); diff --git a/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java b/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java index 9f590dc18078..d72dafeaabc8 100644 --- a/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java +++ b/server/src/test/java/org/apache/cloudstack/service/NetrisServiceMockTest.java @@ -23,6 +23,8 @@ import com.cloud.network.netris.NetrisService; import com.cloud.network.vpc.StaticRoute; import com.cloud.network.vpc.Vpc; +import com.cloud.offering.NetworkOffering; +import com.cloud.utils.net.NetUtils; import java.util.List; @@ -48,17 +50,17 @@ public boolean deleteVpcResource(long zoneId, long accountId, long domainId, Vpc } @Override - public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, Boolean globalRouting) { + public boolean createVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, NetworkOffering.NetworkMode networkMode, NetUtils.InternetProtocol internetProtocol, boolean isL2) { return true; } @Override - public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName) { + public boolean updateVnetResource(Long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String prevNetworkName, boolean isL2) { return true; } @Override - public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr) { + public boolean deleteVnetResource(long zoneId, long accountId, long domainId, String vpcName, Long vpcId, String networkName, Long networkId, String cidr, boolean isL2) { return true; } diff --git a/ui/src/components/view/DetailsTab.vue b/ui/src/components/view/DetailsTab.vue index 135ea7384fa5..bd980924018c 100644 --- a/ui/src/components/view/DetailsTab.vue +++ b/ui/src/components/view/DetailsTab.vue @@ -381,7 +381,17 @@ export default { } return null }, + isNetrisNetwork () { + if (!this.resource.service) { + return false + } + const networkAclService = this.resource.service.find(svc => svc.name === 'NetworkACL') + return networkAclService && networkAclService.provider && networkAclService.provider.some(p => p.name === 'Netris') + }, ip4routes () { + if (this.isNetrisNetwork) { + return null + } if (this.resource.ip4routes && this.resource.ip4routes.length > 0) { var routes = [] for (var route of this.resource.ip4routes) { @@ -392,6 +402,9 @@ export default { return null }, ip6routes () { + if (this.isNetrisNetwork) { + return null + } if (this.resource.ip6routes && this.resource.ip6routes.length > 0) { var routes = [] for (var route of this.resource.ip6routes) { diff --git a/ui/src/config/section/network.js b/ui/src/config/section/network.js index acc7424c9f0c..a8727811fb6a 100644 --- a/ui/src/config/section/network.js +++ b/ui/src/config/section/network.js @@ -803,6 +803,7 @@ export default { }, { name: 'vpn', component: shallowRef(defineAsyncComponent(() => import('@/views/network/VpnDetails.vue'))), + networkServiceFilter: networkService => networkService.filter(x => x.name === 'Vpn').length > 0, show: (record) => { return record.issourcenat || record.virtualmachinetype === 'DomainRouter' || !record.hasrules } }, { diff --git a/ui/src/config/section/offering.js b/ui/src/config/section/offering.js index 4a32619b8c2f..1433533beab8 100644 --- a/ui/src/config/section/offering.js +++ b/ui/src/config/section/offering.js @@ -508,7 +508,7 @@ export default { searchFilters: ['name', 'zoneid', 'domainid'], resourceType: 'VpcOffering', columns: ['name', 'state', 'displaytext', 'domain', 'zone', 'order'], - details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'], + details: ['name', 'id', 'displaytext', 'internetprotocol', 'distributedvpcrouter', 'redundantrouter', 'tags', 'routingmode', 'specifyasnumber', 'service', 'fornsx', 'networkmode', 'domain', 'zone', 'created'], related: [{ name: 'vpc', title: 'label.vpc', diff --git a/ui/src/views/network/VpcTab.vue b/ui/src/views/network/VpcTab.vue index 12e21cd8ec94..f8f3c88e78d4 100644 --- a/ui/src/views/network/VpcTab.vue +++ b/ui/src/views/network/VpcTab.vue @@ -252,7 +252,7 @@ - + - + { - const vpnGateways = response.listvpngatewaysresponse.vpngateway || [] - for (const vpnGateway of vpnGateways) { - if (vpnGateway.publicip === this.resource.ipaddress) { - this.vpnGateway = vpnGateway - } + getAPI('listVPCs', { id: this.resource.vpcid, listAll: true }).then(vpcResponse => { + const vpc = vpcResponse.listvpcsresponse?.vpc?.[0] + if (vpc?.service?.some(s => s.name === 'Vpn')) { + this.vpnGatewayEnabled = true + getAPI('listVpnGateways', { + vpcid: this.resource.vpcid, + listAll: true + }).then(response => { + const vpnGateways = response.listvpngatewaysresponse.vpngateway || [] + for (const vpnGateway of vpnGateways) { + if (vpnGateway.publicip === this.resource.ipaddress) { + this.vpnGateway = vpnGateway + } + } + }).catch(error => { + console.log(error) + this.$notifyError(error) + }) } }).catch(error => { console.log(error) diff --git a/ui/src/views/offering/AddNetworkOffering.vue b/ui/src/views/offering/AddNetworkOffering.vue index 2b3275a11ed5..cab886a761a5 100644 --- a/ui/src/views/offering/AddNetworkOffering.vue +++ b/ui/src/views/offering/AddNetworkOffering.vue @@ -60,7 +60,7 @@ {{ $t('label.isolated') }} - + {{ $t('label.l2') }} @@ -311,7 +311,7 @@ - + - + @@ -374,8 +374,8 @@ name="vmautoscalingcapability" ref="vmautoscalingcapability" :label="$t('label.supportsvmautoscaling')" - v-if="lbServiceChecked && ['Netscaler', 'VirtualRouter', 'VpcVirtualRouter'].includes(lbServiceProvider)"> - + v-if="lbServiceChecked && ['Netscaler', 'VirtualRouter', 'VpcVirtualRouter', 'Netris'].includes(lbServiceProvider)"> + { + return { + ...service, + provider: (service.provider || []).map(provider => ({ ...provider })) + } + }) }).finally(() => { this.supportedServiceLoading = false this.updateSupportedServices() @@ -902,7 +908,12 @@ export default { }, updateSupportedServices () { this.supportedServiceLoading = true - var supportedServices = this.supportedServices + var supportedServices = (this.supportedSvcs.length > 0 ? this.supportedSvcs : this.supportedServices).map(service => { + return { + ...service, + provider: (service.provider || []).map(provider => ({ ...provider })) + } + }) var self = this if (this.provider !== 'NSX' && this.provider !== 'Netris') { if (this.networkmode === 'ROUTED' && this.guestType === 'isolated') { @@ -930,12 +941,10 @@ export default { } }) setTimeout(() => { - self.supportedSvcs = self.supportedServices self.supportedServices = supportedServices self.supportedServiceLoading = false }, 50) } else { - supportedServices = this.supportedSvcs supportedServices = supportedServices.filter(svc => { if (this.provider === 'NSX') { return Object.keys(this.nsxSupportedServicesMap).includes(svc.name) @@ -959,88 +968,93 @@ export default { } return svc }) - self.supportedSvcs = self.supportedServices + + const externalSelectedProviders = {} + supportedServices.forEach(svc => { + const providerName = svc.provider?.[0]?.name + if (providerName) { + externalSelectedProviders[svc.name] = providerName + } + }) + this.selectedServiceProviderMap = externalSelectedProviders + this.sourceNatServiceChecked = 'SourceNat' in externalSelectedProviders + this.lbServiceChecked = 'Lb' in externalSelectedProviders + this.lbServiceProvider = this.lbServiceChecked ? externalSelectedProviders.Lb : '' + if (this.lbServiceProvider === 'Netris') { + this.form.vmautoscalingcapability = true + } + this.staticNatServiceChecked = 'StaticNat' in externalSelectedProviders + this.staticNatServiceProvider = this.staticNatServiceChecked ? externalSelectedProviders.StaticNat : '' + this.connectivityServiceChecked = 'Connectivity' in externalSelectedProviders + this.firewallServiceChecked = 'Firewall' in externalSelectedProviders + this.firewallServiceProvider = this.firewallServiceChecked ? externalSelectedProviders.Firewall : '' + const selectedProviders = Object.values(externalSelectedProviders) + this.isVirtualRouterForAtLeastOneService = selectedProviders.includes('VirtualRouter') + this.isVpcVirtualRouterForAtLeastOneService = selectedProviders.includes('VpcVirtualRouter') + if ((this.isVirtualRouterForAtLeastOneService || this.isVpcVirtualRouterForAtLeastOneService) && + this.serviceOfferings.length === 0) { + this.fetchServiceOfferingData() + } + self.supportedServices = supportedServices self.supportedServiceLoading = false } }, + refreshExternalSupportedServicesMap () { + const selectedProvider = this.form.provider || this.provider + if (selectedProvider !== 'NSX' && selectedProvider !== 'Netris') { + return + } + + const selectedNetworkMode = this.form.networkmode || this.networkmode + const effectiveNetworkMode = selectedProvider === 'Netris' && !selectedNetworkMode ? 'NATTED' : selectedNetworkMode + const externalProvider = selectedProvider === 'NSX' ? this.NSX : this.Netris + const isNsxProvider = selectedProvider === 'NSX' + const commonServices = { + Dhcp: this.forVpc ? this.VPCVR : this.VR, + Dns: this.forVpc ? this.VPCVR : this.VR, + UserData: this.forVpc ? this.VPCVR : this.VR, + ...(this.forVpc && { NetworkACL: externalProvider }), + ...(!this.forVpc && { Firewall: externalProvider }) + } + + const nattedServices = { + SourceNat: externalProvider, + StaticNat: externalProvider, + PortForwarding: externalProvider, + Vpn: this.forVpc ? this.VPCVR : this.VR, + ...((!isNsxProvider || this.form.nsxsupportlb) && { Lb: externalProvider }) + } + + const serviceMap = { + ...commonServices, + ...(effectiveNetworkMode === 'NATTED' && nattedServices) + } + + if (isNsxProvider) { + this.nsxSupportedServicesMap = serviceMap + } else { + this.netrisSupportedServicesMap = serviceMap + } + }, handleForVpcChange (forVpc) { this.forVpc = forVpc - if (this.provider === 'NSX') { - this.nsxSupportedServicesMap = { - Dhcp: this.forVpc ? this.VPCVR : this.VR, - Dns: this.forVpc ? this.VPCVR : this.VR, - UserData: this.forVpc ? this.VPCVR : this.VR, - ...(this.networkmode === 'NATTED' && { - SourceNat: this.NSX, - StaticNat: this.NSX, - PortForwarding: this.NSX, - Lb: this.NSX - }), - ...(forVpc && { NetworkACL: this.NSX }), - ...(!forVpc && { Firewall: this.NSX }) - } - } else if (this.provider === 'Netris') { - this.netrisSupportedServicesMap = { - Dhcp: this.forVpc ? this.VPCVR : this.VR, - Dns: this.forVpc ? this.VPCVR : this.VR, - UserData: this.forVpc ? this.VPCVR : this.VR, - ...(this.networkmode === 'NATTED' && { - SourceNat: this.Netris, - StaticNat: this.Netris, - PortForwarding: this.Netris, - Lb: this.Netris - }), - ...(forVpc && { NetworkACL: this.Netris }), - ...(!forVpc && { Firewall: this.Netris }) - } - } + this.refreshExternalSupportedServicesMap() this.updateSupportedServices() }, handleProviderChange (provider) { this.provider = provider - if (this.provider === 'NSX') { - this.nsxSupportedServicesMap = { - Dhcp: this.forVpc ? this.VPCVR : this.VR, - Dns: this.forVpc ? this.VPCVR : this.VR, - UserData: this.forVpc ? this.VPCVR : this.VR, - ...(this.networkmode === 'NATTED' && { - SourceNat: this.NSX, - StaticNat: this.NSX, - PortForwarding: this.NSX, - Lb: this.NSX - }), - ...(this.forVpc && { NetworkACL: this.NSX }), - ...(!this.forVpc && { Firewall: this.NSX }) - } - } else if (this.provider === 'Netris') { - this.netrisSupportedServicesMap = { - Dhcp: this.forVpc ? this.VPCVR : this.VR, - Dns: this.forVpc ? this.VPCVR : this.VR, - UserData: this.forVpc ? this.VPCVR : this.VR, - ...(this.networkmode === 'NATTED' && { - SourceNat: this.Netris, - StaticNat: this.Netris, - PortForwarding: this.Netris, - Lb: this.Netris - }), - ...(this.forVpc && { NetworkACL: this.Netris }), - ...(!this.forVpc && { Firewall: this.Netris }) - } - } + this.refreshExternalSupportedServicesMap() this.fetchSupportedServiceData() }, handleForNetworkModeChange (networkMode) { this.networkmode = networkMode + this.refreshExternalSupportedServicesMap() this.fetchSupportedServiceData() }, handleNsxLbService (supportLb) { - if (!supportLb && 'Lb' in this.nsxSupportedServicesMap) { - delete this.nsxSupportedServicesMap.Lb - } - if (supportLb && !('Lb' in this.nsxSupportedServicesMap)) { - this.nsxSupportedServicesMap.Lb = this.NSX - } + this.form.nsxsupportlb = supportLb + this.refreshExternalSupportedServicesMap() this.fetchSupportedServiceData() }, handleLbTypeChange (lbType) { @@ -1055,6 +1069,10 @@ export default { this.fetchRegisteredServicePackageData() if (provider != null & provider !== undefined) { this.lbServiceProvider = provider + this.lbServiceChecked = true + if (provider === 'Netris') { + this.form.vmautoscalingcapability = true + } } } else { this.lbServiceProvider = '' @@ -1062,7 +1080,7 @@ export default { this.lbServiceChecked = checked } else if (service === 'StaticNat') { this.staticNatServiceChecked = checked - if (checked && provider != null & provider !== undefined) { + if (checked && provider != null && provider !== undefined) { this.staticNatServiceProvider = provider } else { this.staticNatServiceProvider = '' @@ -1071,13 +1089,13 @@ export default { this.connectivityServiceChecked = checked } else if (service === 'Firewall') { this.firewallServiceChecked = checked - if (checked && provider != null & provider !== undefined) { - this.staticNatServiceProvider = provider + if (checked && provider != null && provider !== undefined) { + this.firewallServiceProvider = provider } else { - this.staticNatServiceProvider = '' + this.firewallServiceProvider = '' } } - if (checked && provider != null & provider !== undefined) { + if (checked && provider != null && provider !== undefined) { this.selectedServiceProviderMap[service] = provider } else { delete this.selectedServiceProviderMap[service] @@ -1196,16 +1214,19 @@ export default { delete params.supportspublicaccess } if (supportedServices.includes('SourceNat')) { + const isNetrisSourceNat = this.selectedServiceProviderMap.SourceNat === 'Netris' if (values.redundantroutercapability === true) { params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'SourceNat' params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter' params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true serviceCapabilityIndex++ } - params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'SourceNat' - params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedSourceNatTypes' - params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.sourcenattype - serviceCapabilityIndex++ + if (!isNetrisSourceNat) { + params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'SourceNat' + params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'SupportedSourceNatTypes' + params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.sourcenattype + serviceCapabilityIndex++ + } delete params.redundantroutercapability delete params.sourcenattype } else if (values.redundantroutercapability === true) { @@ -1233,7 +1254,7 @@ export default { if ('vmautoscalingcapability' in values) { params['servicecapabilitylist[' + serviceCapabilityIndex + '].service'] = 'lb' params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilitytype'] = 'VmAutoScaling' - params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = values.vmautoscalingcapability + params['servicecapabilitylist[' + serviceCapabilityIndex + '].capabilityvalue'] = this.selectedServiceProviderMap.Lb === 'Netris' ? true : values.vmautoscalingcapability serviceCapabilityIndex++ } if (values.elasticlb === true) { diff --git a/ui/src/views/offering/AddVpcOffering.vue b/ui/src/views/offering/AddVpcOffering.vue index d909cbdc3dca..3515ff9e9dd2 100644 --- a/ui/src/views/offering/AddVpcOffering.vue +++ b/ui/src/views/offering/AddVpcOffering.vue @@ -166,7 +166,7 @@ - + @@ -297,6 +297,7 @@ export default { isVpcVirtualRouterForAtLeastOneService: false, connectivityServiceChecked: false, sourceNatServiceChecked: false, + gatewayServiceChecked: false, selectedServiceProviderMap: {}, ipv6NetworkOfferingEnabled: false, routedNetworkEnabled: false, @@ -423,6 +424,8 @@ export default { }, fetchSupportedServiceData () { var services = [] + this.sourceNatServiceChecked = false + this.gatewayServiceChecked = false if (this.provider === 'NSX') { services.push({ name: 'Dhcp', @@ -509,6 +512,11 @@ export default { enabled: true, provider: [{ name: 'VpcVirtualRouter' }] }) + services.push({ + name: 'Vpn', + enabled: true, + provider: [{ name: 'VpcVirtualRouter' }] + }) } else { services.push({ name: 'Dhcp', @@ -600,6 +608,13 @@ export default { }) } } + + if (this.Netris.name === this.form.provider && services.filter(svc => svc.name === 'SourceNat' && svc.provider[0].name === this.Netris.name).length > 0) { + this.sourceNatServiceChecked = true + } + if (this.Netris.name === this.form.provider && services.filter(svc => svc.name === 'Gateway').length > 0) { + this.gatewayServiceChecked = true + } for (var i in services) { services[i].description = services[i].name } @@ -615,6 +630,10 @@ export default { this.form.nsxsupportlb = true this.handleNsxLbService(true) } + if (this.provider === 'Netris') { + this.form.networkmode = 'NATTED' + this.networkmode = 'NATTED' + } this.fetchSupportedServiceData() }, handleNsxLbService (supportLb) { @@ -753,7 +772,10 @@ export default { params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true serviceCapabilityIndex++ } else if (values.redundantrouter === true) { - params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = 'Gateway' + // For Netris NATTED (where SourceNat is auto-added server-side but not in the local map), + // use SourceNat. For all other cases (e.g. ROUTED VPC VR), use Gateway. + const redundantRouterService = (forNetris && values.networkmode !== 'ROUTED') ? 'SourceNat' : 'Gateway' + params['serviceCapabilityList[' + serviceCapabilityIndex + '].service'] = redundantRouterService params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilitytype'] = 'RedundantRouter' params['serviceCapabilityList[' + serviceCapabilityIndex + '].capabilityvalue'] = true serviceCapabilityIndex++