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


##########
server/src/main/java/com/cloud/network/vpc/NetworkACLManagerImpl.java:
##########
@@ -442,12 +446,23 @@ public boolean applyACLItemsToNetwork(final long 
networkId, final List<NetworkAC
             logger.debug("Applying NetworkACL for network: {} with Network ACL 
service provider", network);
             handled = element.applyNetworkACLs(network, rules);
             if (handled) {
-                // publish message on message bus, so that network elements 
implementing distributed routing
-                // capability can act on the event
-                _messageBus.publish(_name, "Network_ACL_Replaced", 
PublishScope.LOCAL, network);
                 break;
             }
         }
+        if (!foundProvider) {
+            // Get provider name and get the element by provider name (it 
could be an external provider)
+            String aclProviderName = 
networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), 
Service.NetworkACL);
+            if (aclProviderName != null) {
+                foundProvider = true;
+                NetworkElement element = 
_networkModel.getElementImplementingProvider(aclProviderName);
+                handled = ((NetworkACLServiceProvider) 
element).applyNetworkACLs(network, rules);
+            }

Review Comment:
   The external-provider fallback path casts `element` without checking for 
null or interface compatibility. If `getElementImplementingProvider` returns 
null (or a NetworkElement that doesn't implement `NetworkACLServiceProvider`), 
this will throw at runtime. Add null/`instanceof` checks and return `false` (or 
throw a controlled exception) when the provider can't handle NetworkACL.



##########
engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java:
##########
@@ -4438,6 +4482,8 @@ public Map<String, String> 
finalizeServicesAndProvidersForNetwork(final NetworkO
 
             if (provider == null) {
                 provider = 
_networkModel.getDefaultUniqueProviderForService(service).getName();
+            } else {
+                provider = _networkModel.resolveProvider(provider).getName();

Review Comment:
   Possible NullPointerException: `_networkModel.resolveProvider(provider)` may 
return null, but the code immediately calls `.getName()`. Guard against null 
(and return a clear Unsupported/Invalid provider error) to avoid failures when 
an invalid provider string is stored in the offering service map.
   ```suggestion
                   final Provider resolvedProvider = 
_networkModel.resolveProvider(provider);
                   if (resolvedProvider == null) {
                       throw new InvalidParameterValueException("Invalid 
provider " + provider + " configured for service " + service);
                   }
                   provider = resolvedProvider.getName();
   ```



##########
ui/src/config/section/network.js:
##########
@@ -202,6 +202,20 @@ export default {
             }
           }
         },
+        {
+          api: 'runCustomAction',
+          icon: 'thunderbolt-outlined',
+          label: 'label.run.action',
+          dataView: true,
+          show: (record) => {
+            return 'runCustomAction' in store.getters.apis &&
+              'listCustomActions' in store.getters.apis &&
+              record.service && record.service.some(s =>
+              s.provider && s.provider.some(p => p.name === 'ExternalNetwork'))
+          },

Review Comment:
   The `runCustomAction` action visibility check hard-codes provider name 
`ExternalNetwork`, but extension-backed providers are selected by extension/NSP 
name (and the server-side constant introduced is `NetworkExtension`). As 
written, the action will likely never appear for extension-backed networks. 
Update the predicate to detect extension-backed providers (e.g., via provider 
name `NetworkExtension` or by matching any provider name that is a 
NetworkOrchestrator extension).



##########
ui/src/views/offering/AddNetworkOffering.vue:
##########
@@ -1029,9 +1078,46 @@ export default {
           ...(this.forVpc && { NetworkACL: this.Netris }),
           ...(!this.forVpc && { Firewall: this.Netris })
         }
+      } else if (this.isExternalNetworkProvider) {
+        // Extension-backed provider: services come from the extension's 
network.capabilities.
+        // this.provider is the extension name (= NSP name)
+        const extProviderObj = {
+          name: this.provider,
+          description: this.provider,
+          enabled: true
+        }
+        const svcMap = { Dhcp: this.VR, Dns: this.VR, UserData: this.VR }
+        // Infer services from the selected extension's network.capabilities 
detail
+        const extDef = this.availableExtensionProviders.find(e => e.name === 
this.provider)
+        const services = this._getExtensionServices(extDef)
+        if (services.length > 0) {
+          services.forEach(svc => {
+            if (!['Dhcp', 'Dns', 'UserData'].includes(svc)) {
+              svcMap[svc] = extProviderObj
+            }
+          })
+        } else {
+          // Default services if no capabilities declared
+          svcMap.SourceNat = extProviderObj
+          svcMap.StaticNat = extProviderObj
+          svcMap.PortForwarding = extProviderObj
+          svcMap.Firewall = extProviderObj
+          svcMap.Gateway = extProviderObj
+        }
+        this.externalNetworkSupportedServicesMap = svcMap
+        this.externalNetworkProviderObj = extProviderObj
       }
       this.fetchSupportedServiceData()
     },
+    _getExtensionServices (extDef) {
+      if (!extDef || !extDef.details || 
!extDef.details['network.capabilities']) return []
+      try {
+        const caps = JSON.parse(extDef.details['network.capabilities'])
+        return (caps && caps.services) ? caps.services : []

Review Comment:
   `_getExtensionServices` only reads `extDef.details['network.capabilities']`. 
Elsewhere (e.g. AddVpcOffering) there is a fallback to `network.services` CSV; 
without that fallback, extensions that only set `network.services` won't have 
their services reflected in the offering UI. Consider supporting both formats 
here for consistency.



##########
server/src/main/java/com/cloud/network/NetworkModelImpl.java:
##########
@@ -1161,7 +1266,7 @@ public Map<Service, Set<Provider>> 
getNetworkOfferingServiceProvidersMap(long ne
             if (providers == null) {
                 providers = new HashSet<Provider>();
             }
-            providers.add(Provider.getProvider(instance.getProvider()));
+            providers.add(resolveProvider(instance.getProvider()));
             serviceProviderMap.put(Service.getService(service), providers);

Review Comment:
   Possible NPE/data corruption: `resolveProvider(instance.getProvider())` can 
return null, but the null value is added to the providers set. This can later 
trigger NPEs when code assumes every provider in the set is non-null. Skip null 
providers (and/or throw an exception if the DB contains an invalid provider 
name).



##########
framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/manager/ExtensionsManagerImpl.java:
##########
@@ -934,6 +1161,247 @@ public ExtensionResourceMap 
registerExtensionWithCluster(Cluster cluster, Extens
         return result;
     }
 
+    protected ExtensionResourceMap 
registerExtensionWithPhysicalNetwork(PhysicalNetworkVO physicalNetwork,
+                  Extension extension, Map<String, String> details) {
+        // Only NetworkOrchestrator extensions can be registered with physical 
networks
+        if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) {
+            throw new InvalidParameterValueException(String.format(
+                    "Only extensions of type %s can be registered with a 
physical network. "
+                    + "Extension '%s' is of type %s.",
+                    Extension.Type.NetworkOrchestrator.name(),
+                    extension.getName(), extension.getType().name()));
+        }
+
+        // Block registering the exact same extension twice on the same 
physical network
+        final ExtensionResourceMap.ResourceType resourceType = 
ExtensionResourceMap.ResourceType.PhysicalNetwork;
+        List<ExtensionResourceMapVO> existing = 
extensionResourceMapDao.listByResourceIdAndType(
+                physicalNetwork.getId(), resourceType);
+        if (existing != null) {
+            for (ExtensionResourceMapVO ex : existing) {
+                if (ex.getExtensionId() == extension.getId()) {
+                    throw new CloudRuntimeException(String.format(
+                            "Extension '%s' is already registered with 
physical network %s",
+                            extension.getName(), physicalNetwork.getId()));
+                }
+            }
+        }
+
+        // Resolve which services this extension provides from its 
network.services detail
+        Set<String> services = resolveExtensionServices(extension);
+
+        return 
Transaction.execute((TransactionCallbackWithException<ExtensionResourceMap, 
CloudRuntimeException>) status -> {
+            // 1. Persist the extension<->physical-network mapping
+            ExtensionResourceMapVO extensionMap = new 
ExtensionResourceMapVO(extension.getId(),
+                    physicalNetwork.getId(), resourceType);
+            ExtensionResourceMapVO savedExtensionMap = 
extensionResourceMapDao.persist(extensionMap);
+
+            // 2. Persist device credentials / details
+            List<ExtensionResourceMapDetailsVO> detailsVOList = new 
ArrayList<>();
+            if (MapUtils.isNotEmpty(details)) {
+                for (Map.Entry<String, String> entry : details.entrySet()) {
+                    boolean display = 
!SENSITIVE_DETAIL_KEYS.contains(entry.getKey().toLowerCase());
+                    detailsVOList.add(new 
ExtensionResourceMapDetailsVO(savedExtensionMap.getId(),
+                            entry.getKey(), entry.getValue(), display));
+                }
+                extensionResourceMapDetailsDao.saveDetails(detailsVOList);
+            }
+
+            // 3. Auto-create the NetworkServiceProvider entry for this 
extension so that
+            //    the services are visible in the UI and in 
listSupportedNetworkServices.
+            //    The NSP name equals the extension name; state is Enabled by 
default.
+            PhysicalNetworkServiceProviderVO existingNsp =
+                    physicalNetworkServiceProviderDao.findByServiceProvider(
+                            physicalNetwork.getId(), extension.getName());
+            if (existingNsp == null) {
+                PhysicalNetworkServiceProviderVO nsp =
+                        new 
PhysicalNetworkServiceProviderVO(physicalNetwork.getId(), extension.getName());
+                applyServicesToNsp(nsp, services);
+                nsp.setState(PhysicalNetworkServiceProvider.State.Enabled);
+                physicalNetworkServiceProviderDao.persist(nsp);
+                logger.info("Auto-created NetworkServiceProvider '{}' 
(Enabled) for physical network {} "
+                        + "with services {}", extension.getName(), 
physicalNetwork.getId(), services);
+            }
+
+            return extensionMap;
+        });
+    }
+
+    /**
+     * Resolves the set of network service names declared in the extension's
+     * {@code network.services} detail. Falls back to an empty set if not 
present
+     */
+    private Set<String> resolveExtensionServices(Extension extension) {
+        Map<String, String> extDetails = 
extensionDetailsDao.listDetailsKeyPairs(extension.getId());
+        Set<String> parsed = parseServicesFromDetailKeys(extDetails);
+        if (!parsed.isEmpty()) {
+            return parsed;
+        }
+        // Default: the full set of services NetworkExtensionElement supports
+        return new HashSet<>();
+    }
+
+    /**
+     * Resolves the set of service names from the extension detail map.
+     * From {@code network.services} comma-separated key.
+     */
+    @SuppressWarnings("deprecation")
+    private Set<String> parseServicesFromDetailKeys(Map<String, String> 
extDetails) {
+        if (extDetails == null) {
+            return Collections.emptySet();
+        }
+        // New format: "network.services" = "SourceNat,StaticNat,..."
+        if 
(extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
+            String value = 
extDetails.get(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY);
+            if (StringUtils.isNotBlank(value)) {
+                Set<String> services = new HashSet<>();
+                for (String s : value.split(",")) {
+                    String trimmed = s.trim();
+                    if (!trimmed.isEmpty()) {
+                        services.add(trimmed);
+                    }
+                }
+                if (!services.isEmpty()) {
+                    return services;
+                }
+            }
+        }
+
+        return Collections.emptySet();
+    }
+
+    /**
+     * Builds a full {@code Map<Service, Map<Capability, String>>} from the
+     * extension detail map.  From the split keys
+     * {@code network.services} + {@code network.service.capabilities}.
+     */
+    @SuppressWarnings("deprecation")
+    private Map<Service, Map<Capability, String>> 
buildCapabilitiesFromDetailKeys(
+            Map<String, String> extDetails) {
+        if (extDetails == null) {
+            return new HashMap<>();
+        }
+        // New split format
+        if 
(extDetails.containsKey(ExtensionHelper.NETWORK_SERVICES_DETAIL_KEY)) {
+            Set<String> serviceNames = parseServicesFromDetailKeys(extDetails);
+            if (!serviceNames.isEmpty()) {
+                JsonObject capsObj = null;
+                if 
(extDetails.containsKey(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY))
 {
+                    try {
+                        capsObj = JsonParser.parseString(
+                                
extDetails.get(ExtensionHelper.NETWORK_SERVICE_CAPABILITIES_DETAIL_KEY))
+                                .getAsJsonObject();
+                    } catch (Exception e) {
+                        logger.warn("Failed to parse 
network.service.capabilities JSON: {}", e.getMessage());
+                    }
+                }

Review Comment:
   The code only parses `network.services` / `network.service.capabilities`, 
but both the UI (e.g. offering creation) and `NetworkExtensionElement` 
documentation refer to a single `network.capabilities` JSON detail. If an 
extension only provides `network.capabilities`, service flags and capabilities 
will be empty and the auto-created NSP will not reflect the extension’s 
declared services. Consider adding parsing support for `network.capabilities` 
(or updating all callers/docs to consistently use the split keys).



##########
server/src/main/java/com/cloud/network/lb/LoadBalancingRulesManagerImpl.java:
##########
@@ -2049,6 +2050,14 @@ protected boolean applyLbRules(Network network, 
List<LoadBalancingRule> rules) t
             if (handled)
                 break;
         }
+        if (!handled) {
+            // Get provider name and get the element by provider name (it 
could be an external provider)
+            String lbProviderName = 
_ntwkSrvcDao.getProviderForServiceInNetwork(network.getId(), Service.Lb);
+            if (lbProviderName != null) {
+                NetworkElement element = 
_networkModel.getElementImplementingProvider(lbProviderName);
+                handled = ((LoadBalancingServiceProvider) 
element).applyLBRules(network, rules);
+            }

Review Comment:
   The external-provider fallback path casts `element` without checking for 
null or `instanceof LoadBalancingServiceProvider`. If the provider name is 
present in the service map but no element implements it (or it doesn't support 
LB), this will throw a ClassCastException/NPE. Add null/`instanceof` checks and 
handle the unsupported case gracefully.



##########
server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java:
##########
@@ -700,18 +718,34 @@ public boolean applyRules(Network network, Purpose 
purpose, List<? extends Firew
                 if (handled)
                     break;
             }
+            if (!handled) {
+                // Get provider name and get the element by provider name (it 
could be an external provider)
+                String fwProviderName = 
networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), 
Service.Firewall);
+                if (fwProviderName != null) {
+                    NetworkElement element = 
_networkModel.getElementImplementingProvider(fwProviderName);
+                    handled = ((FirewallServiceProvider) 
element).applyFWRules(network, rules);
+                }
+            }
             break;
         case PortForwarding:
-                for (PortForwardingServiceProvider element : _pfElements) {
+            for (PortForwardingServiceProvider element : _pfElements) {
                 Network.Provider provider = element.getProvider();
                 boolean  isPfProvider = 
_networkModel.isProviderSupportServiceInNetwork(network.getId(), 
Service.PortForwarding, provider);
                 if (!isPfProvider) {
                     continue;
                 }
-                    handled = element.applyPFRules(network, 
(List<PortForwardingRule>)rules);
+                handled = element.applyPFRules(network, 
(List<PortForwardingRule>)rules);
                 if (handled)
                     break;
             }
+            if (!handled) {
+                // Get provider name and get the element by provider name (it 
could be an external provider)
+                String pfProviderName = 
networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), 
Service.PortForwarding);
+                if (pfProviderName != null) {
+                    NetworkElement element = 
_networkModel.getElementImplementingProvider(pfProviderName);
+                    handled = ((PortForwardingServiceProvider) 
element).applyPFRules(network, (List<PortForwardingRule>) rules);
+                }

Review Comment:
   The extension-provider fallback casts `element` to 
`PortForwardingServiceProvider` without checking for null or `instanceof`. If 
the provider name isn't backed by a PF-capable element, this will throw at 
runtime. Add null/`instanceof` checks and handle the unsupported case 
gracefully.



##########
server/src/main/java/com/cloud/network/firewall/FirewallManagerImpl.java:
##########
@@ -700,18 +718,34 @@ public boolean applyRules(Network network, Purpose 
purpose, List<? extends Firew
                 if (handled)
                     break;
             }
+            if (!handled) {
+                // Get provider name and get the element by provider name (it 
could be an external provider)
+                String fwProviderName = 
networkServiceMapDao.getProviderForServiceInNetwork(network.getId(), 
Service.Firewall);
+                if (fwProviderName != null) {
+                    NetworkElement element = 
_networkModel.getElementImplementingProvider(fwProviderName);
+                    handled = ((FirewallServiceProvider) 
element).applyFWRules(network, rules);
+                }

Review Comment:
   The extension-provider fallback casts `element` to `FirewallServiceProvider` 
without checking for null or `instanceof`. If `networkServiceMapDao` returns a 
provider name with no matching element (or one that doesn't implement 
FirewallServiceProvider), this will throw at runtime. Add null/`instanceof` 
checks and treat it as "not handled" (or throw a controlled exception).



##########
server/src/main/java/com/cloud/network/vpc/VpcManagerImpl.java:
##########
@@ -1844,6 +1854,8 @@ private Map<String, List<String>> 
finalizeServicesAndProvidersForVpc(final long
             if (provider == null) {
                 // Default to VPCVirtualRouter
                 provider = Provider.VPCVirtualRouter.getName();
+            } else {
+                provider = _ntwkModel.resolveProvider(provider).getName();
             }

Review Comment:
   Possible NullPointerException: `_ntwkModel.resolveProvider(provider)` may 
return null for unknown provider names, but the code immediately calls 
`.getName()`. Add a null check and throw an `InvalidParameterValueException` 
(or keep the original provider string) when resolution fails.



##########
engine/orchestration/src/main/java/org/apache/cloudstack/engine/orchestration/NetworkOrchestrator.java:
##########
@@ -4463,7 +4509,7 @@ private List<Provider> getNetworkProviders(final long 
networkId) {
         final List<String> providerNames = 
_ntwkSrvcDao.getDistinctProviders(networkId);
         final List<Provider> providers = new ArrayList<>();
         for (final String providerName : providerNames) {
-            providers.add(Network.Provider.getProvider(providerName));
+            providers.add(_networkModel.resolveProvider(providerName));

Review Comment:
   `getNetworkProviders` adds the result of `resolveProvider` without filtering 
nulls. If any provider name is not resolvable, this list will contain null and 
can cause downstream NPEs if the providers list is iterated (or logged) 
elsewhere. Consider skipping null providers (and optionally logging a warning).
   ```suggestion
               final Provider provider = 
_networkModel.resolveProvider(providerName);
               if (provider != null) {
                   providers.add(provider);
               }
   ```



##########
server/src/main/java/com/cloud/api/ApiResponseHelper.java:
##########
@@ -3302,7 +3304,7 @@ public ProviderResponse 
createNetworkServiceProviderResponse(PhysicalNetworkServ
         }
         response.setServices(services);
 
-        Provider serviceProvider = 
Provider.getProvider(result.getProviderName());
+        Provider serviceProvider = 
networkModel.resolveProvider(result.getProviderName());
         boolean canEnableIndividualServices = 
ApiDBUtils.canElementEnableIndividualServices(serviceProvider);
         response.setCanEnableIndividualServices(canEnableIndividualServices);
 

Review Comment:
   `networkModel.resolveProvider(...)` can return null, but the result is 
passed into `ApiDBUtils.canElementEnableIndividualServices(...)` which will NPE 
on null. Consider calling the new 
`ApiDBUtils.canElementEnableIndividualServicesByName(result.getProviderName())`,
 or guard against null and default `canEnableIndividualServices` to `false`.



-- 
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