Copilot commented on code in PR #13591:
URL: https://github.com/apache/cloudstack/pull/13591#discussion_r4155801935


##########
plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java:
##########
@@ -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;
+                    }

Review Comment:
   The unconditional legacy fallback can delete a different static-NAT rule. 
When the requested IP-suffixed rule is absent but the VM still has a legacy 
rule for another public IP, this lookup selects and deletes that legacy rule. 
Verify that the legacy rule's NAT/public IP equals `cmd.getNatIp()` before 
falling back.



##########
server/src/main/java/com/cloud/network/IpAddressManagerImpl.java:
##########
@@ -1425,15 +1450,19 @@ 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));
             ex.addProxyObject(zone.getUuid(), "zoneId");
             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<Long> vlanDbIds = ipaddress == null ? 
getNetrisVlanDbIds(zone) : null;

Review Comment:
   This restriction is based only on whether Netris exists in the zone, not on 
the network/provider for which the IP is being acquired. 
`NetworkService.allocateIP` accepts any `networkId` but calls this method 
without passing that context, so an unspecified-IP request for a normal 
VR-backed network in a mixed zone is also forced into the Netris VLANs (or 
fails if none are available). Apply Netris steering only when the target 
network/VPC actually uses Netris, while retaining the generic pool for other 
offerings.



##########
plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java:
##########
@@ -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;

Review Comment:
   Treating any legacy rule for the VM as the same rule breaks the new 
multiple-static-NAT support. If an upgraded VM already has a legacy rule for 
public IP A, creating static NAT for secondary public IP B finds the shared 
legacy name and returns success without creating B. Only accept the legacy rule 
when its NAT/public IP matches this command; otherwise create the new 
IP-suffixed rule.



##########
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')

Review Comment:
   This only recognizes VPC Netris networks because it looks specifically at 
NetworkACL. A non-VPC Netris routed network exposes Netris through 
Firewall/other services, so its upstream-route warning is still shown even 
though this change intends to hide it for Netris routed networks. Detect Netris 
in any service provider instead.



##########
ui/src/views/offering/AddNetworkOffering.vue:
##########
@@ -60,7 +60,7 @@
             <a-radio-button value="isolated">
               {{ $t('label.isolated') }}
             </a-radio-button>
-            <a-radio-button value="l2" v-if="form.provider !== 'NSX' && 
form.provider !== 'Netris'">
+            <a-radio-button value="l2" v-if="form.provider !== 'NSX'">

Review Comment:
   The newly exposed Netris L2 option cannot produce a valid offering. This 
form omits `networkmode` for L2, while 
`CreateNetworkOfferingCmd.getSupportedServices()` treats every external 
offering as routed/isolated and generates Dhcp, Dns, UserData, and Firewall 
rather than a single Netris Connectivity service; 
`NetworkOrchestrator.checkL2OfferingServices()` then rejects that service set. 
Add an L2-specific service/provider path across the form and command before 
exposing this choice.



##########
ui/src/views/offering/AddNetworkOffering.vue:
##########
@@ -958,88 +967,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,

Review Comment:
   This adds VPN to the shared NATTED service map for NSX as well as Netris. 
The NSX UI will therefore display/select VPN even though 
`CreateNetworkOfferingCmd.getSupportedServices()` only enables it for Netris, 
so the created offering silently lacks the service shown in the form. Restrict 
this entry to Netris.



##########
plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java:
##########
@@ -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;

Review Comment:
   An existing VPC does not prove that its required IPAM allocation exists. If 
allocation creation failed and rollback also failed, the next retry takes this 
early return, reports success, and later subnet/vNet creation fails against the 
incomplete VPC indefinitely. On the idempotent path, look up or create the 
expected CIDR allocation before returning success.



##########
engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java:
##########
@@ -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

Review Comment:
   The validator immediately below still only accepts no services or 
UserData-only: a sole Connectivity service enters this condition because 
UserData is absent and throws "only UserData service is allowed." Consequently 
the documented Netris Connectivity case remains unusable; update the validation 
(including provider verification) to actually allow exactly Connectivity/Netris.



##########
plugins/network-elements/netris/src/main/java/org/apache/cloudstack/service/NetrisApiClientImpl.java:
##########
@@ -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);
+                }

Review Comment:
   For L2, `getOrCreateL2Vpc()` may have just created a dedicated VPC, but both 
this failure branch and the catch block explicitly skip rollback when `isL2` is 
true. A rejected vNet creation (including name-length validation or API 
failure) therefore leaves an orphan Netris VPC. Track whether the L2 VPC was 
created by this attempt and delete it on failure, without deleting a 
pre-existing retry target.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to