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


##########
framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java:
##########
@@ -0,0 +1,2728 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+package org.apache.cloudstack.framework.extensions.network;
+
+import java.io.File;
+import java.net.URI;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.file.Files;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.inject.Inject;
+import javax.naming.ConfigurationException;
+
+import com.cloud.agent.api.to.LoadBalancerTO;
+import com.cloud.dc.DataCenter;
+import com.cloud.dc.Vlan;
+import com.cloud.dc.VlanVO;
+import com.cloud.dc.dao.DataCenterDao;
+import com.cloud.dc.dao.VlanDao;
+import com.cloud.deploy.DeployDestination;
+import com.cloud.exception.ConcurrentOperationException;
+import com.cloud.exception.InsufficientCapacityException;
+import com.cloud.exception.ResourceUnavailableException;
+import com.cloud.host.dao.HostDao;
+import com.cloud.hypervisor.Hypervisor;
+import com.cloud.network.IpAddressManager;
+import com.cloud.network.IpAddress;
+import com.cloud.network.Network;
+import com.cloud.network.Network.Capability;
+import com.cloud.network.Network.Provider;
+import com.cloud.network.Network.Service;
+import com.cloud.network.NetworkModel;
+import com.cloud.network.Networks;
+import com.cloud.network.dao.NetworkDetailVO;
+import com.cloud.network.dao.NetworkDao;
+import com.cloud.network.dao.NetworkVO;
+import com.cloud.network.dao.PhysicalNetworkDao;
+import com.cloud.network.dao.PhysicalNetworkVO;
+import com.cloud.network.PhysicalNetworkServiceProvider;
+import com.cloud.network.PublicIpAddress;
+import com.cloud.network.addr.PublicIp;
+import com.cloud.network.dao.FirewallRulesDao;
+import com.cloud.network.dao.IPAddressDao;
+import com.cloud.network.dao.IPAddressVO;
+import com.cloud.network.dao.NetworkDetailsDao;
+import com.cloud.network.dao.NetworkServiceMapDao;
+import com.cloud.network.vpc.dao.VpcDao;
+import com.cloud.network.element.AggregatedCommandExecutor;
+import com.cloud.network.element.DhcpServiceProvider;
+import com.cloud.network.element.DnsServiceProvider;
+import com.cloud.network.element.FirewallServiceProvider;
+import com.cloud.network.element.IpDeployer;
+import com.cloud.network.element.LoadBalancingServiceProvider;
+import com.cloud.network.element.NetworkACLServiceProvider;
+import com.cloud.network.element.NetworkElement;
+import com.cloud.network.element.PortForwardingServiceProvider;
+import com.cloud.network.element.SourceNatServiceProvider;
+import com.cloud.network.element.StaticNatServiceProvider;
+import com.cloud.network.element.UserDataServiceProvider;
+import com.cloud.network.element.VpcProvider;
+import com.cloud.network.vpc.NetworkACLItem;
+import com.cloud.network.vpc.PrivateGateway;
+import com.cloud.network.vpc.StaticRouteProfile;
+import com.cloud.network.vpc.Vpc;
+import com.cloud.network.lb.LoadBalancingRule;
+import com.cloud.network.rules.FirewallRule;
+import com.cloud.network.rules.FirewallRuleVO;
+import com.cloud.network.rules.PortForwardingRule;
+import com.cloud.network.rules.StaticNat;
+import com.cloud.offerings.NetworkOfferingVO;
+import com.cloud.offerings.dao.NetworkOfferingDao;
+import com.cloud.service.ServiceOfferingVO;
+import com.cloud.service.dao.ServiceOfferingDao;
+import com.cloud.storage.dao.GuestOSCategoryDao;
+import com.cloud.storage.dao.GuestOSDao;
+import com.cloud.user.AccountService;
+import com.cloud.uservm.UserVm;
+import com.cloud.offering.NetworkOffering;
+import com.cloud.user.Account;
+import com.cloud.utils.Pair;
+import com.cloud.utils.component.AdapterBase;
+import com.cloud.utils.exception.CloudRuntimeException;
+import com.cloud.vm.Nic;
+import com.cloud.vm.NicProfile;
+import com.cloud.vm.NicVO;
+import com.cloud.vm.ReservationContext;
+import com.cloud.vm.UserVmVO;
+import com.cloud.vm.VirtualMachine;
+import com.cloud.vm.VirtualMachineManager;
+import com.cloud.vm.VirtualMachineProfile;
+import com.cloud.vm.VMInstanceDetailVO;
+import com.cloud.vm.VmDetailConstants;
+import com.cloud.vm.dao.NicDao;
+import com.cloud.vm.dao.UserVmDao;
+import com.cloud.vm.dao.VMInstanceDetailsDao;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParser;
+
+import org.apache.cloudstack.api.ApiConstants;
+import 
org.apache.cloudstack.engine.orchestration.service.NetworkOrchestrationService;
+import org.apache.cloudstack.extension.Extension;
+import org.apache.cloudstack.extension.ExtensionHelper;
+import org.apache.cloudstack.extension.NetworkCustomActionProvider;
+import org.apache.cloudstack.framework.extensions.dao.ExtensionDetailsDao;
+import org.apache.cloudstack.resourcedetail.dao.VpcDetailsDao;
+import org.apache.commons.lang3.EnumUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.stream.Collectors;
+
+
+public class NetworkExtensionElement extends AdapterBase implements
+        NetworkElement, SourceNatServiceProvider, StaticNatServiceProvider,
+        PortForwardingServiceProvider, IpDeployer, NetworkCustomActionProvider,
+        DhcpServiceProvider, DnsServiceProvider, FirewallServiceProvider,
+        UserDataServiceProvider, LoadBalancingServiceProvider,
+        VpcProvider, NetworkACLServiceProvider, AggregatedCommandExecutor {
+
+    private static final Map<Service, Map<Capability, String>> 
DEFAULT_CAPABILITIES = new HashMap<>();
+
+
+    /**
+     * When non-null, restricts all operations to the extension whose name
+     * matches this provider name.
+     */
+    private String providerName;
+
+    @Inject
+    private NetworkModel networkModel;
+    @Inject
+    private NetworkServiceMapDao ntwkSrvcDao;
+    @Inject
+    private ExtensionHelper extensionHelper;
+    @Inject
+    private NetworkDetailsDao networkDetailsDao;
+    @Inject
+    private IpAddressManager ipAddressManager;
+    @Inject
+    private NetworkOrchestrationService networkManager;
+    @Inject
+    private AccountService accountService;
+    @Inject
+    private PhysicalNetworkDao physicalNetworkDao;
+    @Inject
+    private ExtensionDetailsDao extensionDetailsDao;
+    @Inject
+    private NetworkDao networkDao;
+    @Inject
+    private DataCenterDao dataCenterDao;
+    @Inject
+    private VlanDao vlanDao;
+    @Inject
+    private GuestOSCategoryDao guestOSCategoryDao;
+    @Inject
+    private GuestOSDao guestOSDao;
+    @Inject
+    private HostDao hostDao;
+    @Inject
+    private VMInstanceDetailsDao vmInstanceDetailsDao;
+    @Inject
+    private UserVmDao userVmDao;
+    @Inject
+    private NicDao nicDao;
+    @Inject
+    private NetworkOfferingDao networkOfferingDao;
+    @Inject
+    private ServiceOfferingDao serviceOfferingDao;
+    @Inject
+    private FirewallRulesDao firewallRulesDao;
+    @Inject
+    private IPAddressDao ipAddressDao;
+    @Inject
+    private VpcDao vpcDao;
+    @Inject
+    private VpcDetailsDao vpcDetailsDao;
+
+    // ---- Script argument names ----
+
+    public static final String ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS = 
"physical-network-extension-details";
+    public static final String ARG_NETWORK_EXTENSION_DETAILS = 
"network-extension-details";
+    public static final String ARG_PAYLOAD = "payload";
+    public static final String ARG_ACTION_PARAMS = "action-params";
+
+    public static final int DEFAULT_SCRIPT_TIMEOUT_SECONDS = 60;
+
+    public static final int EXIT_CODE_SUCCESS = 0;
+    public static final int EXIT_CODE_FAILURE = -1;
+
+    // ---- Script command names ----
+
+    public static final String CMD_IMPLEMENT_NETWORK = "implement-network";
+    public static final String CMD_SHUTDOWN_NETWORK = "shutdown-network";
+    public static final String CMD_DESTROY_NETWORK = "destroy-network";
+    public static final String CMD_ENSURE_NETWORK_DEVICE = 
"ensure-network-device";
+    public static final String CMD_ASSIGN_IP = "assign-ip";
+    public static final String CMD_RELEASE_IP = "release-ip";
+    public static final String CMD_ADD_STATIC_NAT = "add-static-nat";
+    public static final String CMD_DELETE_STATIC_NAT = "delete-static-nat";
+    public static final String CMD_ADD_PORT_FORWARD = "add-port-forward";
+    public static final String CMD_DELETE_PORT_FORWARD = "delete-port-forward";
+    public static final String CMD_ADD_DHCP_ENTRY = "add-dhcp-entry";
+    public static final String CMD_CONFIG_DHCP_SUBNET = "config-dhcp-subnet";
+    public static final String CMD_REMOVE_DHCP_SUBNET = "remove-dhcp-subnet";
+    public static final String CMD_SET_DHCP_OPTIONS = "set-dhcp-options";
+    public static final String CMD_REMOVE_DHCP_ENTRY = "remove-dhcp-entry";
+    public static final String CMD_ADD_DNS_ENTRY = "add-dns-entry";
+    public static final String CMD_CONFIG_DNS_SUBNET = "config-dns-subnet";
+    public static final String CMD_REMOVE_DNS_SUBNET = "remove-dns-subnet";
+    public static final String CMD_SAVE_VM_DATA = "save-vm-data";
+    public static final String CMD_SAVE_PASSWORD = "save-password";
+    public static final String CMD_SAVE_USERDATA = "save-userdata";
+    public static final String CMD_SAVE_SSHKEY = "save-sshkey";
+    public static final String CMD_SAVE_HYPERVISOR_HOSTNAME = 
"save-hypervisor-hostname";
+    public static final String CMD_APPLY_LB_RULES = "apply-lb-rules";
+    public static final String CMD_APPLY_FW_RULES = "apply-fw-rules";
+    public static final String CMD_RESTORE_NETWORK = "restore-network";
+    public static final String CMD_IMPLEMENT_VPC = "implement-vpc";
+    public static final String CMD_SHUTDOWN_VPC = "shutdown-vpc";
+    public static final String CMD_UPDATE_VPC_SOURCE_NAT_IP = 
"update-vpc-source-nat-ip";
+    public static final String CMD_APPLY_NETWORK_ACL = "apply-network-acl";
+    public static final String CMD_CUSTOM_ACTION = "custom-action";
+
+    // ---- Network detail key ----
+
+    /**
+     * Key used to persist the per-network JSON blob in {@code 
network_details}.
+     * The blob is produced by the network-extension.sh's {@code 
ensure-network-device}
+     * command and may contain any fields the script needs (e.g. selected host,
+     * namespace name, VRF ID, …).
+     */
+    public static final String NETWORK_DETAIL_EXTENSION_DETAILS = 
"extension.details";
+
+    public String getProviderName() {
+        return providerName;
+    }
+
+    /**
+     * Returns a new {@link NetworkExtensionElement} scoped to {@code 
providerName},
+     * sharing all injected dependencies with this instance.
+     */
+    public NetworkExtensionElement withProviderName(String providerName) {
+        NetworkExtensionElement copy = new NetworkExtensionElement();
+        copy.networkModel                   = this.networkModel;
+        copy.ntwkSrvcDao                    = this.ntwkSrvcDao;
+        copy.extensionHelper                = this.extensionHelper;
+        copy.networkDetailsDao              = this.networkDetailsDao;
+        copy.ipAddressManager               = this.ipAddressManager;
+        copy.physicalNetworkDao             = this.physicalNetworkDao;
+        copy.extensionDetailsDao            = this.extensionDetailsDao;
+        copy.networkDao                     = this.networkDao;
+        copy.dataCenterDao                  = this.dataCenterDao;
+        copy.vlanDao                        = this.vlanDao;
+        copy.guestOSCategoryDao             = this.guestOSCategoryDao;
+        copy.guestOSDao                     = this.guestOSDao;
+        copy.hostDao                        = this.hostDao;
+        copy.vmInstanceDetailsDao           = this.vmInstanceDetailsDao;
+        copy.userVmDao                      = this.userVmDao;
+        copy.nicDao                         = this.nicDao;
+        copy.networkManager                 = this.networkManager;
+        copy.networkOfferingDao             = this.networkOfferingDao;
+        copy.serviceOfferingDao             = this.serviceOfferingDao;
+        copy.accountService                 = this.accountService;
+        copy.firewallRulesDao               = this.firewallRulesDao;
+        copy.ipAddressDao                   = this.ipAddressDao;
+        copy.vpcDao                         = this.vpcDao;
+        copy.vpcDetailsDao                  = this.vpcDetailsDao;
+        copy.providerName                   = providerName;
+
+        logger.debug("NetworkExtensionElement initialised with provider name 
'{}'", providerName);
+        return copy;
+    }
+
+    // ---- Capabilities ----
+
+    @Override
+    public Map<Service, Map<Capability, String>> getCapabilities() {
+        try {
+            // If this element is scoped to a provider name, prefer 
capabilities stored
+            // in the extension's "network.service.capabilities" detail.  The 
ExtensionHelper
+            // exposes a helper that loads the Service→Capability map from the 
DB.
+            if (providerName != null && !providerName.isBlank()) {
+                Map<Service, Map<Capability, String>> caps = 
extensionHelper.getNetworkCapabilitiesForProvider(null, providerName);
+                if (caps != null && !caps.isEmpty()) {
+                    return caps;
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("Failed to load network service capabilities from 
extension details for provider '{}': {}", providerName, e.getMessage());
+        }
+
+        return DEFAULT_CAPABILITIES;
+    }
+
+    @Override
+    public Provider getProvider() {
+        if (providerName != null) {
+            return Provider.createTransientProvider(providerName);
+        }
+        return Provider.NetworkExtension;
+    }
+
+    // ---- Extension / provider resolution ----
+
+    protected Extension resolveExtension(Network network) {
+        Long physicalNetworkId = network.getPhysicalNetworkId();
+        if (physicalNetworkId == null) {
+            logger.warn("Network {} has no physical network — cannot resolve 
extension", network.getId());
+            return null;
+        }
+        if (providerName != null && !providerName.isBlank()) {
+            Extension ext = 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, 
providerName);
+            if (ext != null) {
+                return ext;
+            }
+            logger.warn("No extension found for scoped provider '{}' on 
physical network {}", providerName, physicalNetworkId);
+        }
+        List<String> providers = 
ntwkSrvcDao.getDistinctProviders(network.getId());
+        if (providers != null) {
+            for (String p : providers) {
+                Extension ext = 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, p);
+                if (ext != null) {
+                    return ext;
+                }
+            }
+        }
+        return null;
+    }
+
+    protected boolean canHandle(Network network, Service service) {
+        Long physicalNetworkId = network.getPhysicalNetworkId();
+        if (physicalNetworkId == null) {
+            return false;
+        }
+        if (providerName != null && !providerName.isBlank()) {
+            boolean hasExt = 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, 
providerName) != null;
+            if (!hasExt) {
+                return false;
+            }
+            if (service == null) {
+                return true;
+            }
+            List<String> sp = 
ntwkSrvcDao.getProvidersForServiceInNetwork(network.getId(), service);
+            return sp != null && sp.stream()
+                    .anyMatch(p -> 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, p) 
!= null);
+        }
+        List<String> providers = 
ntwkSrvcDao.getDistinctProviders(network.getId());
+        if (providers == null || providers.isEmpty()) {
+            return false;
+        }
+        boolean hasExtProv = providers.stream().anyMatch(
+                p -> 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, p) 
!= null);
+        if (!hasExtProv) {
+            return false;
+        }
+        if (service == null) {
+            return true;
+        }
+        List<String> sp = 
ntwkSrvcDao.getProvidersForServiceInNetwork(network.getId(), service);
+        return sp != null && sp.stream()
+                .anyMatch(p -> 
extensionHelper.getExtensionForPhysicalNetworkAndProvider(physicalNetworkId, p) 
!= null);
+    }
+
+    @Override
+    public boolean configure(String name, Map<String, Object> params) throws 
ConfigurationException {
+        super.configure(name, params);
+        return true;
+    }
+
+    // ---- NetworkElement lifecycle ----
+
+    @Override
+    public boolean implement(Network network, NetworkOffering offering, 
DeployDestination dest,
+            ReservationContext context) throws ConcurrentOperationException,
+            ResourceUnavailableException, InsufficientCapacityException {
+        if (!canHandle(network, null)) {
+            return false;
+        }
+        logger.info("Implementing network extension for network {} (VLAN {})", 
network.getId(), network.getBroadcastUri());
+
+        // Step 1: Ensure a network device is selected and its details stored.
+        ensureExtensionDetails(network);
+
+        // Step 2: Allocate the IPs for DHCP/DNS/UserData service if needed
+        String extensionIp = ensureExtensionIp(network);
+
+        String vlanId = getVlanId(network);
+
+        // Step 2: Create the network on the device.
+        JsonObject implementPayload = new JsonObject();
+        implementPayload.addProperty("network_id", 
String.valueOf(network.getId()));
+        implementPayload.addProperty("vlan", safeStr(vlanId));
+        implementPayload.addProperty("gateway", safeStr(network.getGateway()));
+        implementPayload.addProperty("cidr", safeStr(network.getCidr()));
+        implementPayload.addProperty("extension_ip", safeStr(extensionIp));
+        addVpcIdToPayload(implementPayload, network);
+
+        Pair<Integer, String> result = executeScriptAndReturnOutput(network, 
CMD_IMPLEMENT_NETWORK, implementPayload);
+
+        if (result.first() != EXIT_CODE_SUCCESS) {
+            return false;
+        }
+
+        // Update the network properties from the output
+        applyNetworkUpdateFromScriptOutput(network, result.second());
+
+        // Step 3: Configure source NAT for both VPC and non-VPC networks for
+        // compatibility (other network-element providers may also implement 
VPC tiers).
+        // When this is a VPC tier, the script's assign-ip does nothing for 
source-nat
+        // because VPC source NAT is managed at the VPC level by 
implementVpc().
+        if (canHandle(network, Service.SourceNat)) {
+            try {
+                if (network.getVpcId() == null) {
+                    // Isolated network: apply the network's own source NAT IP.
+                    Account owner = 
accountService.getAccount(network.getAccountId());
+                    PublicIpAddress existingIp = 
networkModel.getSourceNatIpAddressForGuestNetwork(owner, network);
+                    if (existingIp != null) {
+                        applyIps(network, List.of(existingIp), 
Set.of(Service.SourceNat));
+                    }
+                } else {
+                    // VPC tier: apply the VPC-level source NAT IP (script is 
a no-op for SNAT).
+                    final PublicIpAddress vpcSourceNatIp = 
getVpcSourceNatIp(network.getVpcId());
+                    if (vpcSourceNatIp != null) {
+                        applyIps(network, List.of(vpcSourceNatIp), 
Set.of(Service.SourceNat));
+                    }
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to configure source NAT IP for network {}: 
{}", network.getId(), e.getMessage(), e);
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean prepare(Network network, NicProfile nic, 
VirtualMachineProfile vm,
+            DeployDestination dest, ReservationContext context)
+            throws ConcurrentOperationException, ResourceUnavailableException, 
InsufficientCapacityException {
+        // Copy from VirtualRouterElement.java
+        if (vm.getType() != VirtualMachine.Type.User || vm.getHypervisorType() 
== Hypervisor.HypervisorType.BareMetal) {
+            return false;
+        }
+
+        if (!canHandle(network, null)) {
+            return false;
+        }
+
+        if 
(!networkModel.isProviderEnabledInPhysicalNetwork(networkModel.getPhysicalNetworkId(network),
 getProvider().getName())) {
+            return false;
+        }
+
+        // Sync nic with network
+        applyNicUpdateFromNetwork(network, nic.getId());
+
+        final NetworkOfferingVO offering = 
networkOfferingDao.findById(network.getNetworkOfferingId());
+        implement(network, offering, dest, context);
+
+        return true;
+    }
+
+    private void applyNicUpdateFromNetwork(Network network, Long nicId) {
+        if (nicId == null) {
+            return;
+        }
+        try {
+            NicVO nicVo = nicDao.findById(nicId);
+            if (nicVo == null) {
+                return;
+            }
+            if (network.getBroadcastUri() != null) {
+                nicVo.setBroadcastUri(network.getBroadcastUri());
+                nicVo.setIsolationUri(network.getBroadcastUri());
+            }
+            nicDao.update(nicId, nicVo);
+        } catch (Exception e) {
+            logger.debug("Failed to update nic {}: {}", nicId, e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean release(Network network, NicProfile nic, 
VirtualMachineProfile vm,
+            ReservationContext context) throws ConcurrentOperationException, 
ResourceUnavailableException {
+        return true;
+    }
+
+    @Override
+    public boolean shutdown(Network network, ReservationContext context, 
boolean cleanup)
+            throws ConcurrentOperationException, ResourceUnavailableException {
+        logger.info("Shutting down network extension for network {}", 
network.getId());
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("vlan", safeStr(getVlanId(network)));
+        addVpcIdToPayload(payload, network);
+        boolean result = executeScript(network, CMD_SHUTDOWN_NETWORK, payload);
+        if (result) {
+            // Remove stored per-network extension details (e.g. namespace). 
For VPC-backed networks
+            // the namespace is named cs-vpc-<vpcId>, stored in the extension 
details. Removing the
+            // stored details ensures the namespace is deleted/forgotten on 
shutdown.
+            try {
+                networkDetailsDao.removeDetail(network.getId(), 
NETWORK_DETAIL_EXTENSION_DETAILS);
+            } catch (Exception e) {
+                logger.warn("Failed to remove network extension details for 
network {}: {}", network.getId(), e.getMessage());
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean destroy(Network network, ReservationContext context)
+            throws ConcurrentOperationException, ResourceUnavailableException {
+        logger.info("Destroying network extension for network {}", 
network.getId());
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("vlan", safeStr(getVlanId(network)));
+        addVpcIdToPayload(payload, network);
+        // For both isolated and VPC tier networks, use destroy-network.
+        // For VPC tiers, the script preserves the shared namespace;
+        // the VPC namespace is removed only when shutdownVpc() calls 
shutdown-vpc.
+        boolean result = executeScript(network, CMD_DESTROY_NETWORK, payload);
+        if (result) {
+            cleanupPlaceholderNicIp(network, context);
+            networkDetailsDao.removeDetail(network.getId(), 
NETWORK_DETAIL_EXTENSION_DETAILS);
+        }
+        return result;
+    }
+
+    /**
+     * Releases placeholder NIC IPs allocated for DHCP/DNS/UserData extension 
traffic,
+     * then removes the placeholder NIC record(s) for this network.
+     */
+    protected void cleanupPlaceholderNicIp(Network network, ReservationContext 
context) {
+        List<NicVO> placeholderNics = 
nicDao.listPlaceholderNicsByNetworkIdAndVmType(
+                network.getId(), VirtualMachine.Type.DomainRouter);
+        if (placeholderNics == null || placeholderNics.isEmpty()) {
+            return;
+        }
+
+        long userId = accountService.getSystemUser().getId();
+        Account caller = accountService.getSystemAccount();
+        if (context != null && context.getAccount() != null) {
+            caller = context.getAccount();
+        }
+
+        for (NicVO placeholderNic : placeholderNics) {
+            try {
+                String ip = placeholderNic.getIPv4Address();
+                if (ip != null && !ip.isBlank()) {
+                    logger.debug("Cleaning up PlaceHolder IP {} on network 
{}", ip, network.getId());
+                    IPAddressVO ipAddress = 
ipAddressDao.findByIpAndSourceNetworkId(network.getId(), ip);
+                    if (ipAddress != null) {
+                        if 
(Network.GuestType.Shared.equals(network.getGuestType())) {
+                            
ipAddressManager.disassociatePublicIpAddress(ipAddress, userId, caller);
+                        } else {
+                            
ipAddressManager.markIpAsUnavailable(ipAddress.getId());
+                            ipAddressDao.unassignIpAddress(ipAddress.getId());
+                        }
+                    }
+                }
+            } catch (Exception e) {
+                logger.warn("Failed to release placeholder IP for network {} 
and nic {}: {}",
+                        network.getId(), placeholderNic.getId(), 
e.getMessage());
+            }
+
+            try {
+                nicDao.remove(placeholderNic.getId());
+            } catch (Exception e) {
+                logger.warn("Failed to remove placeholder nic {} for network 
{}: {}",
+                        placeholderNic.getId(), network.getId(), 
e.getMessage());
+            }
+        }
+    }
+
+    @Override
+    public boolean isReady(PhysicalNetworkServiceProvider provider) {
+        return true;
+    }
+
+    @Override
+    public boolean shutdownProviderInstances(PhysicalNetworkServiceProvider 
provider, ReservationContext context)
+            throws ConcurrentOperationException, ResourceUnavailableException {
+        return true;
+    }
+
+    @Override
+    public boolean canEnableIndividualServices() {
+        return true;
+    }
+
+    @Override
+    public boolean verifyServicesCombination(Set<Service> services) {
+        return true;
+    }
+
+    // ---- ensure-network-device ----
+
+    /**
+     * Calls the network-extension.sh script with {@code 
ensure-network-device} before
+     * the first network operation.  The script verifies the previously 
selected
+     * device is reachable (using the {@code hosts} list in the 
physical-network
+     * extension details) and performs failover if needed.  The returned JSON 
is
+     * persisted in {@code network_details} and forwarded to all subsequent 
calls
+     * via {@value #ARG_NETWORK_EXTENSION_DETAILS}.
+     *
+     * <p>For VPC tier networks the extension details are inherited from the 
VPC-level
+     * details (stored in {@code vpc_details}) so all tiers in the same VPC 
share
+     * the same host/namespace binding.  The script's {@code 
ensure-network-device}
+     * is only called at the VPC level (see {@link 
#ensureExtensionDetails(Vpc)}).</p>
+     */
+    protected void ensureExtensionDetails(Network network) {
+        if (network.getVpcId() != null) {
+            Vpc vpc = vpcDao.findById(network.getVpcId());
+            ensureExtensionDetails(vpc);
+            return;
+        }
+
+        // Isolated network: run ensure-network-device to select / validate 
the host.
+        Map<String, String> stored = 
networkDetailsDao.listDetailsKeyPairs(network.getId());
+        String currentDetails = stored != null
+                ? stored.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") 
: "{}";
+
+        logger.info("Ensuring network device for network {} (current={})", 
network.getId(), currentDetails);
+
+        Extension extension = resolveExtension(network);
+        File scriptFile = resolveScriptFile(network, extension);
+
+        JsonObject argsPayload = new JsonObject();
+        argsPayload.addProperty("network_id", String.valueOf(network.getId()));
+        argsPayload.addProperty("vlan", safeStr(getVlanId(network)));
+        argsPayload.addProperty("zone_id", 
String.valueOf(network.getDataCenterId()));
+        argsPayload.addProperty("current_details", currentDetails);
+        addVpcIdToPayload(argsPayload, network);
+        JsonObject payload = buildNetworkScriptPayload(network, argsPayload, 
extension);
+
+        try {
+            Pair<Integer, String> result = 
executeScriptWithFilePayload(scriptFile,
+                    CMD_ENSURE_NETWORK_DEVICE, payload, "Network extension");
+            String output = result.second() != null ? result.second() : "";
+
+            if (result.first() != EXIT_CODE_SUCCESS) {
+                logger.warn("ensure-network-device exited {} for network {} — 
keeping current details",
+                        -1, network.getId());
+                if ("{}".equals(currentDetails)) {
+                    networkDetailsDao.addDetail(network.getId(), 
NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false);
+                }
+                return;
+            }
+            if (output.isEmpty()) {
+                output = "{}".equals(currentDetails) ? "{}" : currentDetails;
+            }
+            if (!output.equals(currentDetails)) {
+                logger.info("Network device updated for network {}: {}", 
network.getId(), output);
+                networkDetailsDao.addDetail(network.getId(), 
NETWORK_DETAIL_EXTENSION_DETAILS, output, false);
+            } else {
+                logger.debug("Network device unchanged for network {}: {}", 
network.getId(), output);
+            }
+        } catch (Exception e) {
+            logger.warn("Failed ensure-network-device for network {}: {}", 
network.getId(), e.getMessage());
+            if ("{}".equals(currentDetails)) {
+                networkDetailsDao.addDetail(network.getId(), 
NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false);
+            }
+        }
+    }
+
+    /*
+    * If the network supports DHCP/DNS/UserData but not SourceNat/Gateway,
+    * an additional IP is needed on the external network to host these 
services.
+    * This method ensures that IP is allocated and configured on the external 
network and returns its address.
+     */
+    protected String ensureExtensionIp(Network network) {
+        if (networkModel.isAnyServiceSupportedInNetwork(network.getId(), 
this.getProvider(),
+                Service.SourceNat, Service.Gateway)) {
+            // Gateway or Source NAT will be configured on the external network
+            return network.getGateway();
+        }
+
+        if (networkModel.isAnyServiceSupportedInNetwork(network.getId(), 
this.getProvider(),
+                Service.Dhcp, Service.Dns, Service.UserData)) {
+                try {
+                    // An extra IP will be allocated and configured on the 
external network
+                    Nic placeholderNic = 
networkModel.getPlaceholderNicForRouter(network, null);
+                    if (placeholderNic == null) {
+                        NetworkDetailVO routerIpDetail = 
networkDetailsDao.findDetail(network.getId(), ApiConstants.ROUTER_IP);
+                        String routerIp = routerIpDetail != null ? 
routerIpDetail.getValue() : null;
+                        Account account = 
accountService.getAccount(network.getAccountId());
+                        String extensionIp = 
Network.GuestType.Shared.equals(network.getGuestType()) ?
+                                
ipAddressManager.assignPublicIpAddress(network.getDataCenterId(), null, 
account, Vlan.VlanType.DirectAttached, network.getId(), routerIp, false, 
false).getAddress().toString():
+                                
ipAddressManager.acquireGuestIpAddress(network, routerIp);
+                        logger.debug("Saving placeholder nic with ip4 address 
{} for the network", extensionIp, network);
+                        networkManager.savePlaceholderNic(network, 
extensionIp, null, VirtualMachine.Type.DomainRouter);
+                        return extensionIp;
+                    }
+                    return placeholderNic.getIPv4Address();
+                } catch (Exception e) {
+                    logger.warn("Failed to acquire extension IP for network 
{}: {}", network.getId(), e.getMessage());
+                }
+        }
+        return null;
+    }
+
+    // ---- IpDeployer ----
+
+    @Override
+    public boolean applyIps(Network network, List<? extends PublicIpAddress> 
ipAddress, Set<Service> services)
+            throws ResourceUnavailableException {
+        if (ipAddress == null || ipAddress.isEmpty()) {
+            return true;
+        }
+        logger.info("Applying {} IPs for network {}", ipAddress.size(), 
network.getId());
+        String vlanId = getVlanId(network);
+
+        for (PublicIpAddress ip : ipAddress) {
+            boolean isSourceNat = ip.isSourceNat();
+            boolean isRevoke = ip.getState() == IpAddress.State.Releasing;
+            String action = isRevoke ? CMD_RELEASE_IP : CMD_ASSIGN_IP;
+
+            // Public VLAN tag (e.g. "101") from the IP's VLAN record.
+            String publicVlanTag = safeStr(ip.getVlanTag());
+
+            // Compute public IP gateway and CIDR (from the PublicIpAddress if 
available)
+            String publicGateway;
+            String publicCidr;
+            try {
+                publicGateway = ip.getGateway();
+                String publicIpStr = ip.getAddress() != null ? 
ip.getAddress().addr() : null;
+                String publicNetmask = ip.getNetmask();
+                publicCidr = buildCidrFromIpAndNetmask(publicIpStr, 
publicNetmask);
+            } catch (Exception e) {
+                publicGateway = null;
+                publicCidr = null;
+            }
+
+            JsonObject payload = new JsonObject();
+            payload.addProperty("network_id", String.valueOf(network.getId()));
+            payload.addProperty("vlan", safeStr(vlanId));
+            payload.addProperty("public_ip", ip.getAddress().addr());
+            payload.addProperty("source_nat", String.valueOf(isSourceNat));
+            payload.addProperty("gateway", safeStr(network.getGateway()));
+            payload.addProperty("cidr", safeStr(network.getCidr()));
+            payload.addProperty("public_gateway", safeStr(publicGateway));
+            payload.addProperty("public_cidr", safeStr(publicCidr));
+            payload.addProperty("public_vlan", publicVlanTag);
+            addVpcIdToPayload(payload, network);
+
+             boolean result = executeScript(network, action, payload);
+             if (!result) {
+                 throw new ResourceUnavailableException(
+                         "Failed to " + action + " for IP " + 
ip.getAddress().addr(),
+                         Network.class, network.getId());
+             }
+         }
+         return true;
+     }
+
+    /**
+     * Build a CIDR string from IP address and dotted netmask (or prefix).
+     * Returns "" if either value is null or parsing fails.
+     */
+    private String buildCidrFromIpAndNetmask(String ipStr, String netmaskStr) {
+        if (ipStr == null || ipStr.isEmpty() || netmaskStr == null || 
netmaskStr.isEmpty()) {
+            return "";
+        }
+        // If netmask is already CIDR (contains '/'), try to return 
network/prefix
+        if (netmaskStr.contains("/")) {
+            return netmaskStr;
+        }
+        try {
+            InetAddress ip = InetAddress.getByName(ipStr);
+            InetAddress mask = InetAddress.getByName(netmaskStr);
+            int maskInt = ByteBuffer.wrap(mask.getAddress()).getInt();
+            int prefix = Integer.bitCount(maskInt);
+            // Return the provided IP with the calculated prefix so the 
address retains its host value
+            return ip.getHostAddress() + "/" + prefix;
+        } catch (Exception e) {
+            logger.debug("Failed to compute CIDR from ip/netmask {} {}: {}", 
ipStr, netmaskStr, e.getMessage());
+            return "";
+        }
+    }
+
+    // ---- StaticNatServiceProvider ----
+
+    @Override
+    public boolean applyStaticNats(Network config, List<? extends StaticNat> 
rules)
+            throws ResourceUnavailableException {
+        if (rules == null || rules.isEmpty()) {
+            return true;
+        }
+        if (!canHandle(config, Service.StaticNat)) {
+            return false;
+        }
+        logger.info("Applying {} static NAT rules for network {}", 
rules.size(), config.getId());
+        String vlanId = getVlanId(config);
+        for (StaticNat rule : rules) {
+            String action = rule.isForRevoke() ? CMD_DELETE_STATIC_NAT : 
CMD_ADD_STATIC_NAT;
+            String publicCidr = getPublicCidr(rule.getSourceIpAddressId());
+            String publicVlanTag = 
getPublicVlanTag(rule.getSourceIpAddressId());
+
+            JsonObject payload = new JsonObject();
+            payload.addProperty("network_id", String.valueOf(config.getId()));
+            payload.addProperty("vlan", safeStr(vlanId));
+            payload.addProperty("public_ip", 
getIpAddress(rule.getSourceIpAddressId()));
+            payload.addProperty("public_cidr", safeStr(publicCidr));
+            payload.addProperty("public_vlan", publicVlanTag);
+            payload.addProperty("private_ip", 
safeStr(rule.getDestIpAddress()));
+            addVpcIdToPayload(payload, config);
+            boolean result = executeScript(config, action, payload);
+            if (!result) {
+                throw new ResourceUnavailableException("Failed to " + action + 
" for static NAT rule",
+                        Network.class, config.getId());
+            }
+        }
+        return true;
+    }
+
+    // ---- PortForwardingServiceProvider ----
+
+    @Override
+    public boolean applyPFRules(Network network, List<PortForwardingRule> 
rules)
+            throws ResourceUnavailableException {
+        if (rules == null || rules.isEmpty()) {
+            return true;
+        }
+        if (!canHandle(network, Service.PortForwarding)) {
+            return false;
+        }
+        logger.info("Applying {} port forwarding rules for network {}", 
rules.size(), network.getId());
+        String vlanId = getVlanId(network);
+        for (PortForwardingRule rule : rules) {
+            boolean isRevoke = rule.getState() == FirewallRule.State.Revoke;
+            String action = isRevoke ? CMD_DELETE_PORT_FORWARD : 
CMD_ADD_PORT_FORWARD;
+            String publicPort  = 
PortForwardingServiceProvider.getPublicPortRange(rule);
+            String privatePort = 
PortForwardingServiceProvider.getPrivatePFPortRange(rule);
+            String publicCidr  = getPublicCidr(rule.getSourceIpAddressId());
+            String publicVlanTag = 
getPublicVlanTag(rule.getSourceIpAddressId());
+
+            JsonObject payload = new JsonObject();
+            payload.addProperty("network_id", String.valueOf(network.getId()));
+            payload.addProperty("vlan", safeStr(vlanId));
+            payload.addProperty("public_ip", 
getIpAddress(rule.getSourceIpAddressId()));
+            payload.addProperty("public_cidr", safeStr(publicCidr));
+            payload.addProperty("public_vlan", publicVlanTag);
+            payload.addProperty("public_port", safeStr(publicPort));
+            payload.addProperty("private_ip", 
safeStr(rule.getDestinationIpAddress() != null
+                    ? rule.getDestinationIpAddress().addr() : null));
+            payload.addProperty("private_port", safeStr(privatePort));
+            payload.addProperty("protocol", safeStr(rule.getProtocol()));
+            addVpcIdToPayload(payload, network);
+            boolean result = executeScript(network, action, payload);
+            if (!result) {
+                throw new ResourceUnavailableException("Failed to " + action + 
" for port forwarding rule",
+                        Network.class, network.getId());
+            }
+        }
+        return true;
+    }
+
+    // ---- Script execution ----
+
+    protected boolean executeScript(Network network, String command, 
JsonObject argsPayload) {
+        return executeScriptAndReturnOutput(network, command, 
argsPayload).first() == 0;
+    }
+
+    protected Pair<Integer, String> executeScriptAndReturnOutput(Network 
network, String command, JsonObject argsPayload) {
+        ensureExtensionDetails(network);
+        Extension extension = resolveExtension(network);
+        JsonObject payload = buildNetworkScriptPayload(network, argsPayload, 
extension);
+        return executeScriptWithFilePayload(network, command, payload);
+    }
+
+    private JsonObject parseJsonOutput(String outputStr) {
+        if (StringUtils.isBlank(outputStr)) {
+            return null;
+        }
+        try {
+            JsonElement parsed = JsonParser.parseString(outputStr);
+            if (!parsed.isJsonObject()) {
+                logger.debug("Ignoring non-object script output: {}", 
outputStr);
+                return null;
+            }
+            return parsed.getAsJsonObject();
+        } catch (Exception e) {
+            logger.debug("Ignoring non-JSON script output: {}", outputStr);
+            return null;
+        }
+    }
+
+    private String getJsonString(JsonObject jsonObject, String keyPath) {
+        if (jsonObject == null || StringUtils.isBlank(keyPath)) {
+            return null;
+        }
+        JsonElement value = jsonObject.has(keyPath) ? jsonObject.get(keyPath) 
: null;
+        if (value == null) {
+            JsonElement current = jsonObject;
+            String[] parts = keyPath.split("\\.");
+            for (String part : parts) {
+                if (current == null || !current.isJsonObject()) {
+                    current = null;
+                    break;
+                }
+                JsonObject currentObj = current.getAsJsonObject();
+                if (!currentObj.has(part)) {
+                    current = null;
+                    break;
+                }
+                current = currentObj.get(part);
+            }
+            value = current;
+        }
+        if (value == null || value.isJsonNull()) {
+            return null;
+        }
+        return value.getAsString();
+    }
+
+    private void applyNetworkUpdateFromScriptOutput(Network network, String 
outputStr) {
+        JsonObject outputJson = parseJsonOutput(outputStr);
+
+        String networkBroadcastUri = getJsonString(outputJson, 
"network.broadcast_uri");
+        String networkBroadcastDomainType = getJsonString(outputJson, 
"network.broadcast_domain_type");
+        if (networkBroadcastUri == null && networkBroadcastDomainType == null) 
{
+            return;
+        }
+
+        try {
+            NetworkVO networkVo = networkDao.findById(network.getId());
+            if (networkVo == null) {
+                return;
+            }
+
+            boolean changed = false;
+            if (networkBroadcastDomainType != null) {
+                Networks.BroadcastDomainType domainType = 
EnumUtils.getEnumIgnoreCase(Networks.BroadcastDomainType.class, 
networkBroadcastDomainType);
+                if (domainType != null) {
+                    networkVo.setBroadcastDomainType(domainType);
+                    changed = true;
+                } else {
+                    logger.warn("Ignoring unknown broadcast domain type '{}' 
for network {}",
+                            networkBroadcastDomainType, network.getId());
+                }
+            }
+
+            if (networkBroadcastUri != null) {
+                networkVo.setBroadcastUri(URI.create(networkBroadcastUri));
+                changed = true;
+            }
+
+            if (changed) {
+                networkDao.update(networkVo.getId(), networkVo);
+                for (NicVO nicVO : nicDao.listByNetworkId(networkVo.getId())) {
+                    applyNicUpdateFromNetwork(network, nicVO.getId());
+                }
+            }
+        } catch (Exception e) {
+            logger.warn("Failed to update network {} from script output: {}", 
network.getId(), e.getMessage());
+        }
+    }
+
+    protected Pair<Integer, String> executeScriptWithFilePayload(Network 
network, String command, JsonObject payload) {
+        Extension extension = resolveExtension(network);
+        File scriptFile = resolveScriptFile(network, extension);
+        return executeScriptWithFilePayload(scriptFile, command, payload, 
"Network extension");
+    }
+
+    private Pair<Integer, String> executeScriptWithFilePayload(File 
scriptFile, String command,
+            JsonObject payload, String logPrefix) {
+        File payloadFile = null;
+        try {
+            payloadFile = File.createTempFile("cs-extnet-" + command + "-", 
".payload");
+            String payloadJson = payload != null ? new Gson().toJson(payload) 
: "{}";
+            logger.debug("Writing payload to payload file {}", payloadFile);
+            Files.writeString(payloadFile.toPath(), payloadJson, 
StandardCharsets.UTF_8);
+
+            List<String> cmdLine = new ArrayList<>();
+            cmdLine.add(scriptFile.getAbsolutePath());
+            cmdLine.add(command);
+            cmdLine.add(payloadFile.getAbsolutePath());
+            cmdLine.add(String.valueOf(DEFAULT_SCRIPT_TIMEOUT_SECONDS));
+
+            logger.debug("Executing {} script: {}", logPrefix, String.join(" 
", cmdLine));
+
+            ProcessBuilder processBuilder = new ProcessBuilder(cmdLine);
+            processBuilder.redirectErrorStream(true);
+            Process process = processBuilder.start();
+            byte[] output = process.getInputStream().readAllBytes();
+            int exitCode = process.waitFor();
+
+            String outputStr = new String(output).trim();
+            if (!outputStr.isEmpty()) {
+                logger.debug("Script output: {}", outputStr);
+            }
+
+            if (exitCode != EXIT_CODE_SUCCESS) {
+                logger.error("{} script {} failed with exit code {}: {}", 
logPrefix, command, exitCode, outputStr);
+                return new Pair<>(exitCode, outputStr);
+            }
+
+            JsonObject outputJson = parseJsonOutput(outputStr);
+            String status = outputJson != null ? getJsonString(outputJson, 
"status") : null;
+            if (StringUtils.isNotBlank(status) && 
!"success".equalsIgnoreCase(status)) {
+                logger.error("{} script {} returned non-success status '{}': 
{}", logPrefix, command, status, outputStr);
+                return new Pair<>(EXIT_CODE_FAILURE, outputStr);
+            }
+
+            return new Pair<>(EXIT_CODE_SUCCESS, outputStr);
+        } catch (Exception e) {
+            throw new CloudRuntimeException(
+                    String.format("Failed preparing payload file for command 
%s", command), e);
+        } finally {
+            if (payloadFile != null && payloadFile.exists() && 
!payloadFile.delete()) {
+                payloadFile.deleteOnExit();
+            }
+        }
+    }
+
+    private JsonObject buildNetworkScriptPayload(Network network, JsonObject 
argsPayload, Extension extension) {
+        JsonObject payload = new JsonObject();
+        payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS,
+                
buildPhysicalNetworkExtensionDetailsPayload(network.getPhysicalNetworkId(), 
extension));
+        payload.add(ARG_NETWORK_EXTENSION_DETAILS, 
buildNetworkExtensionDetailsPayload(network));
+        payload.add(ARG_PAYLOAD, argsPayload != null ? argsPayload : new 
JsonObject());
+        return payload;
+    }
+
+    private void addVpcIdToPayload(JsonObject payload, Network network) {
+        if (payload != null && network != null && network.getVpcId() != null) {
+            payload.addProperty("vpc_id", String.valueOf(network.getVpcId()));
+        }
+    }
+
+    private void addNicUuidToPayload(JsonObject payload, NicProfile nic) {
+        if (payload != null && nic != null && 
StringUtils.isNotBlank(nic.getUuid())) {
+            payload.addProperty("nic_uuid", nic.getUuid());
+        }
+    }
+
+    // ---- Detail helpers ----
+
+    /**
+     * Returns all {@code extension_resource_map_details} for the given 
extension
+     * on the physical network as a plain map, enriched with physical-network
+     * metadata (name, kvmnetworklabel, vmwarenetworklabel, xennetworklabel,
+     * public_kvmnetworklabel) so the wrapper script can derive bridge names 
and
+     * interface names without extra lookups.
+     */
+    private Map<String, String> buildPhysicalNetworkDetailsMap(Long 
physicalNetworkId, Extension extension) {
+        Map<String, String> details = new HashMap<>();
+        if (physicalNetworkId == null || extension == null) {
+            return details;
+        }
+        // Start with registered extension_resource_map_details
+        Map<String, String> mapDetails = 
extensionHelper.getAllResourceMapDetailsForExtensionOnPhysicalNetwork(
+                physicalNetworkId, extension.getId());
+        if (mapDetails != null) {
+            details.putAll(mapDetails);
+        }
+
+        // Enrich with physical-network record fields
+        PhysicalNetworkVO pn = physicalNetworkDao.findById(physicalNetworkId);
+        if (pn != null && pn.getName() != null) {
+            details.put("physicalnetworkname", pn.getName());
+        }
+
+        return details;
+    }
+
+    /**
+     * Builds the physical-network extension details as a {@link JsonObject}.
+     * Includes all {@code extension_resource_map_details} for the extension 
on the
+     * physical network, enriched with physical-network metadata fields.
+     */
+    private JsonObject buildPhysicalNetworkExtensionDetailsPayload(Long 
physicalNetworkId, Extension extension) {
+        Map<String, String> map = 
buildPhysicalNetworkDetailsMap(physicalNetworkId, extension);
+        JsonObject obj = new JsonObject();
+        for (Map.Entry<String, String> entry : map.entrySet()) {
+            if (entry.getValue() != null) {
+                obj.addProperty(entry.getKey(), entry.getValue());
+            }
+        }
+        return obj;
+    }
+
+    /**
+     * Returns the per-network extension-details JSON blob (the value stored 
under
+     * {@code NETWORK_DETAIL_EXTENSION_DETAILS} in {@code network_details} or
+     * {@code vpc_details}) as a {@link JsonObject}.
+     * Returns an empty object when no blob has been stored yet.
+     */
+    private JsonObject buildNetworkExtensionDetailsPayload(Network network) {
+        String json;
+        if (network.getVpcId() != null) {
+            Map<String, String> vpcDetails = 
vpcDetailsDao.listDetailsKeyPairs(network.getVpcId());
+            json = vpcDetails != null ? 
vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}";
+        } else {
+            Map<String, String> networkDetails = 
networkDetailsDao.listDetailsKeyPairs(network.getId());
+            json = networkDetails != null ? 
networkDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}";
+        }
+        return parseJsonObjectOrEmpty(json);
+    }
+
+    /**
+     * Returns the VPC-level extension-details JSON blob (stored under
+     * {@code NETWORK_DETAIL_EXTENSION_DETAILS} in {@code vpc_details}) as a
+     * {@link JsonObject}.  Returns an empty object when no blob has been 
stored.
+     */
+    private JsonObject buildVpcExtensionDetailsPayload(long vpcId) {
+        Map<String, String> vpcDetails = 
vpcDetailsDao.listDetailsKeyPairs(vpcId);
+        String json = vpcDetails != null ? 
vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}";
+        return parseJsonObjectOrEmpty(json);
+    }
+
+    /**
+     * Builds the custom-action parameters as a {@link JsonObject}.
+     * Returns an empty object for {@code null} or empty parameter maps.
+     */
+    private JsonObject buildActionParamsPayload(Map<String, Object> 
parameters) {
+        JsonObject obj = new JsonObject();
+        if (parameters == null || parameters.isEmpty()) {
+            return obj;
+        }
+        for (Map.Entry<String, Object> entry : parameters.entrySet()) {
+            obj.addProperty(entry.getKey(),
+                    entry.getValue() != null ? entry.getValue().toString() : 
"");
+        }
+        return obj;
+    }
+
+    /**
+     * Parses a JSON string into a {@link JsonObject}.
+     * Returns an empty {@link JsonObject} when the input is {@code null}, 
blank,
+     * or not a valid JSON object.
+     */
+    private JsonObject parseJsonObjectOrEmpty(String json) {
+        if (json == null || json.isBlank()) {
+            return new JsonObject();
+        }
+        try {
+            JsonElement element = JsonParser.parseString(json);
+            return element.isJsonObject() ? element.getAsJsonObject() : new 
JsonObject();
+        } catch (Exception e) {
+            return new JsonObject();
+        }
+    }
+
+    // ---- Custom action ----
+
+    @Override
+    public boolean canHandleCustomAction(Network network) {
+        return canHandle(network, Service.CustomAction);
+    }
+
+    /**
+     * Runs a custom action on the external network device.
+     * The custom action payload is written to a temporary file and passed to 
the
+     * extension script via {@link #executeScriptWithFilePayload(File, String, 
JsonObject, String)}.
+     */
+    @Override
+    public String runCustomAction(Network network, String actionName, 
Map<String, Object> parameters) {
+        Extension extension = resolveExtension(network);
+        File scriptFile = resolveScriptFile(network, extension);
+
+        JsonObject payload = buildCustomActionPayload(network, extension, 
actionName, parameters);
+
+        logger.info("Running custom action '{}' on network {} (extension: {}, 
params: {} key(s))",
+                actionName, network.getId(), extension != null ? 
extension.getName() : "unknown",
+                parameters != null ? parameters.size() : 0);
+
+        try {
+            Pair<Integer, String> result = 
executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload,
+                    "Network extension");
+            String outputStr = result.second() != null ? 
result.second().trim() : "";
+
+            if (result.first() != EXIT_CODE_SUCCESS) {
+                logger.error("Custom action '{}' failed: {}", actionName, 
outputStr);
+                return null;
+            }
+            logger.info("Custom action '{}' completed successfully", 
actionName);
+            return outputStr.isEmpty() ? "OK" : outputStr;
+        } catch (Exception e) {
+            logger.error("Failed to execute custom action '{}': {}", 
actionName, e.getMessage(), e);
+            throw new CloudRuntimeException("Failed to execute custom action: 
" + actionName, e);
+        }
+    }
+
+    @Override
+    public boolean canHandleVpcCustomAction(Vpc vpc) {
+        return resolveExtensionForVpc(vpc) != null;
+    }
+
+    /**
+     * Runs a custom action on the external network device for a VPC.
+     * The custom action payload is written to a temporary file and passed to 
the
+     * extension script via {@link #executeScriptWithFilePayload(File, String, 
JsonObject, String)}.
+     */
+    @Override
+    public String runCustomAction(Vpc vpc, String actionName, Map<String, 
Object> parameters) {
+        Pair<Long, Extension> physNetAndExt = resolveExtensionForVpc(vpc);
+        if (physNetAndExt == null) {
+            throw new CloudRuntimeException("No extension found for VPC " + 
vpc.getId());
+        }
+        Long physicalNetworkId = physNetAndExt.first();
+        Extension extension = physNetAndExt.second();
+        File scriptFile = resolveScriptFileForVpc(physicalNetworkId, 
extension);
+
+        JsonObject payload = buildCustomActionPayload(vpc, physicalNetworkId, 
extension, actionName, parameters);
+
+        logger.info("Running custom action '{}' on VPC {} (extension: {}, 
params: {} key(s))",
+                actionName, vpc.getId(), extension != null ? 
extension.getName() : "unknown",
+                parameters != null ? parameters.size() : 0);
+
+        try {
+            Pair<Integer, String> result = 
executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload,
+                    "VPC extension");
+            String outputStr = result.second() != null ? 
result.second().trim() : "";
+
+            if (result.first() != EXIT_CODE_SUCCESS) {
+                logger.error("VPC custom action '{}' failed: {}", actionName, 
outputStr);
+                return null;
+            }
+            logger.info("VPC custom action '{}' completed successfully", 
actionName);
+            return outputStr.isEmpty() ? "OK" : outputStr;
+        } catch (Exception e) {
+            logger.error("Failed to execute VPC custom action '{}': {}", 
actionName, e.getMessage(), e);
+            throw new CloudRuntimeException("Failed to execute VPC custom 
action: " + actionName, e);
+        }
+    }
+
+    private JsonObject buildCustomActionPayload(Network network, Extension 
extension, String actionName,
+            Map<String, Object> parameters) {
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        addVpcIdToPayload(payload, network);
+        payload.addProperty("action", actionName);
+        payload.add(ARG_ACTION_PARAMS, buildActionParamsPayload(parameters));
+        payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS,
+                
buildPhysicalNetworkExtensionDetailsPayload(network.getPhysicalNetworkId(), 
extension));
+        payload.add(ARG_NETWORK_EXTENSION_DETAILS,
+                buildNetworkExtensionDetailsPayload(network));
+        return payload;
+    }
+
+    private JsonObject buildCustomActionPayload(Vpc vpc, Long 
physicalNetworkId, Extension extension,
+            String actionName, Map<String, Object> parameters) {
+        JsonObject payload = new JsonObject();
+        payload.addProperty("vpc_id", String.valueOf(vpc.getId()));
+        payload.addProperty("action", actionName);
+        payload.add(ARG_ACTION_PARAMS, buildActionParamsPayload(parameters));
+        payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS,
+                buildPhysicalNetworkExtensionDetailsPayload(physicalNetworkId, 
extension));
+        payload.add(ARG_NETWORK_EXTENSION_DETAILS,
+                buildVpcExtensionDetailsPayload(vpc.getId()));
+        return payload;
+    }
+
+    // ---- Script file resolution ----
+
+    /**
+     * Resolves the executable script file from the given extension.
+     *
+     * <p>Lookup order (first match wins):</p>
+     * <ol>
+     *   <li>{@code <extensionPath>/<extensionName>.sh} — preferred convention,
+     *       e.g. for an extension named {@code network-extension} the script 
is
+     *       {@code network-extension.sh}.</li>
+     *   <li>{@code <extensionPath>} itself, if it is a file and is 
executable.</li>
+     * </ol>
+     */
+    protected File resolveScriptFile(Network network, Extension extension) {
+        Long physicalNetworkId = network.getPhysicalNetworkId();
+        if (physicalNetworkId == null) {
+            throw new CloudRuntimeException("Network " + network.getId() + " 
has no physical network");
+        }
+        if (extension == null) {
+            throw new CloudRuntimeException(
+                    "No NetworkOrchestrator extension found for network " + 
network.getId()
+                    + " on physical network " + physicalNetworkId);
+        }
+        if (!Extension.Type.NetworkOrchestrator.equals(extension.getType())) {
+            throw new CloudRuntimeException("Extension " + extension.getName() 
+ " is not of type NetworkOrchestrator");
+        }
+        if (!Extension.State.Enabled.equals(extension.getState())) {
+            throw new CloudRuntimeException("Extension " + extension.getName() 
+ " is not enabled");
+        }
+        if (!extension.isPathReady()) {
+            throw new CloudRuntimeException("Extension " + extension.getName() 
+ " path is not ready");
+        }
+
+        String extensionPath = 
extensionHelper.getExtensionScriptPath(extension);
+        if (extensionPath == null) {
+            throw new CloudRuntimeException("Could not resolve path for 
extension " + extension.getName());
+        }
+
+        File extensionDir = new File(extensionPath);
+        if (extensionDir.isFile() && extensionDir.canExecute()) {
+            return extensionDir;
+        }
+
+        throw new CloudRuntimeException(
+                "No executable script found in extension path " + extensionPath
+                + ". Expected '" + extension.getName() + ".sh' inside the 
extension directory.");
+    }
+
+    // ---- Helpers ----
+
+    private String getVlanId(Network network) {
+        return network.getBroadcastUri() != null
+                ? 
Networks.BroadcastDomainType.getValue(network.getBroadcastUri()) : null;
+    }
+
+    private String getIpAddress(Long ipAddressId) {
+        if (ipAddressId == null) {
+            return "";
+        }
+        IpAddress ip = networkModel.getIp(ipAddressId);
+        return ip != null ? ip.getAddress().addr() : "";
+    }
+
+    private String getPublicCidr(Long ipAddressId) {
+        if (ipAddressId == null) {
+            return "";
+        }
+        IpAddress ip = networkModel.getIp(ipAddressId);
+        if (ip.getAddress() == null) {
+            return "";
+        }
+        VlanVO vlan = vlanDao.findById(ip.getVlanId());
+        return buildCidrFromIpAndNetmask(ip.getAddress().addr(), 
vlan.getVlanNetmask());
+    }
+
+    private String getPublicVlanTag(Long ipAddressId) {
+        if (ipAddressId == null) {
+            return "";
+        }
+        IpAddress ip = networkModel.getIp(ipAddressId);
+        if (ip == null) {
+            return "";
+        }
+        VlanVO vlan = vlanDao.findById(ip.getVlanId());
+        return vlan != null ? safeStr(vlan.getVlanTag()) : "";
+    }
+
+    private String safeStr(String value) {
+        return value != null ? value : "";
+    }
+
+    // ---- DhcpServiceProvider ----
+
+    private String getNetworkDns(final Network network) {
+        final DataCenter dc = 
dataCenterDao.findById(network.getDataCenterId());
+        Pair<String, String> dnsList = networkModel.getNetworkIp4Dns(network, 
dc);
+        return dnsList.first() + (dnsList.second() != null ? "," + 
dnsList.second() : "");
+    }
+
+    @Override
+    public boolean addDhcpEntry(Network network, NicProfile nic, 
VirtualMachineProfile vm,
+            DeployDestination dest, ReservationContext context)
+            throws ConcurrentOperationException, 
InsufficientCapacityException, ResourceUnavailableException {
+        if (!canHandle(network, Service.Dhcp)) {
+            return false;
+        }
+        String extensionIp = ensureExtensionIp(network);
+        logger.debug("addDhcpEntry: network={} mac={} ip={}", network.getId(),
+                nic.getMacAddress(), nic.getIPv4Address());
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("mac", safeStr(nic.getMacAddress()));
+        payload.addProperty("ip", safeStr(nic.getIPv4Address()));
+        payload.addProperty("hostname", safeStr(vm.getHostName()));
+        payload.addProperty("gateway", safeStr(network.getGateway()));
+        payload.addProperty("cidr", safeStr(network.getCidr()));
+        payload.addProperty("dns", safeStr(getNetworkDns(network)));
+        payload.addProperty("default_nic", String.valueOf(nic.isDefaultNic()));
+        payload.addProperty("domain", safeStr(network.getNetworkDomain()));
+        payload.addProperty("extension_ip", safeStr(extensionIp));
+        addNicUuidToPayload(payload, nic);
+        addVpcIdToPayload(payload, network);
+        return executeScript(network, CMD_ADD_DHCP_ENTRY, payload);
+    }
+
+    @Override
+    public boolean configDhcpSupportForSubnet(Network network, NicProfile nic, 
VirtualMachineProfile vm,
+            DeployDestination dest, ReservationContext context)
+            throws ConcurrentOperationException, 
InsufficientCapacityException, ResourceUnavailableException {
+        if (!canHandle(network, Service.Dhcp)) {
+            return false;
+        }
+        logger.debug("configDhcpSupportForSubnet: network={}", 
network.getId());
+        String extensionIp = ensureExtensionIp(network);
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("gateway", safeStr(network.getGateway()));
+        payload.addProperty("cidr", safeStr(network.getCidr()));
+        payload.addProperty("dns", safeStr(getNetworkDns(network)));
+        payload.addProperty("vlan", safeStr(getVlanId(network)));
+        payload.addProperty("domain", safeStr(network.getNetworkDomain()));
+        payload.addProperty("extension_ip", safeStr(extensionIp));
+        addNicUuidToPayload(payload, nic);
+        addVpcIdToPayload(payload, network);
+        return executeScript(network, CMD_CONFIG_DHCP_SUBNET, payload);
+    }
+
+    @Override
+    public boolean removeDhcpSupportForSubnet(Network network) throws 
ResourceUnavailableException {
+        if (!canHandle(network, Service.Dhcp)) {
+            return false;
+        }
+        logger.debug("removeDhcpSupportForSubnet: network={}", 
network.getId());
+        String extensionIp = ensureExtensionIp(network);
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("extension_ip", safeStr(extensionIp));
+        addVpcIdToPayload(payload, network);
+        return executeScript(network, CMD_REMOVE_DHCP_SUBNET, payload);
+    }
+
+    @Override
+    public boolean setExtraDhcpOptions(Network network, long nicId, 
Map<Integer, String> dhcpOptions) {
+        if (!canHandle(network, Service.Dhcp)) {
+            return false;
+        }
+        if (dhcpOptions == null || dhcpOptions.isEmpty()) {
+            return true;
+        }
+        logger.debug("setExtraDhcpOptions: network={} nicId={} options={}", 
network.getId(), nicId, dhcpOptions.size());
+        // Serialise options as a compact JSON object: {"<code>":"<value>", 
...}
+        StringBuilder json = new StringBuilder("{");
+        boolean first = true;
+        for (Map.Entry<Integer, String> e : dhcpOptions.entrySet()) {
+            if (!first) json.append(",");
+            json.append("\"").append(e.getKey()).append("\":\"")
+                .append(e.getValue() != null ? e.getValue().replace("\"", 
"\\\"") : "")
+                .append("\"");
+            first = false;
+        }
+        json.append("}");
+        String extensionIp = ensureExtensionIp(network);
+        JsonObject payload = new JsonObject();
+        payload.addProperty("network_id", String.valueOf(network.getId()));
+        payload.addProperty("nic_id", String.valueOf(nicId));

Review Comment:
   I think it is because the method in DhcpServiceProvider is defined as
   ```
   boolean setExtraDhcpOptions(Network network, long nicId, Map<Integer, 
String> dhcpOptions);
   ```
   
   I will check if these info is better
   - nic uuid
   - nic deviceId
   



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