From b6018da361e92b224e8484b76cab9e46d268265b Mon Sep 17 00:00:00 2001 From: Wei Zhou Date: Wed, 13 May 2026 08:57:57 +0200 Subject: [PATCH] NE: pass physical network and network details and payload in a JSON file --- .../network/NetworkExtensionElement.java | 1120 +++++++---------- .../framework/extensions/network/README.md | 742 ++++++----- 2 files changed, 876 insertions(+), 986 deletions(-) diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java index b2b8862a135..050e9f5b38d 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/NetworkExtensionElement.java @@ -22,8 +22,6 @@ import java.net.InetAddress; import java.nio.ByteBuffer; import java.nio.file.Files; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -134,88 +132,6 @@ import java.util.Base64; import java.util.stream.Collectors; -/** - * NetworkExtensionElement is a network plugin that delegates all network - * configuration to an external script via a registered {@link Extension} of - * type {@code NetworkOrchestrator}. - * - *

Script invocation model

- * The script is called with a command name and optional CLI arguments. - * Two JSON blobs are always forwarded as named CLI arguments: - * - * - *

Script resolution

- * The script is resolved from the extension path set when the extension was - * created. Lookup order (first match wins): - *
    - *
  1. {@code /.sh} — preferred convention, - * e.g. for an extension named {@code network-extension} the script is - * {@code network-extension.sh}.
  2. - *
  3. {@code } itself, if it is a file and is executable.
  4. - *
- * - *

Physical-network extension details

- * Any key/value pairs stored in {@code extension_resource_map_details} at - * registration time are passed verbatim as a JSON object. There are no - * pre-defined keys — the user and the script agree on the schema. The only - * special treatment is that keys named {@code password} or {@code sshkey} are - * redacted in log output. - * - *

Two well-known optional keys control which host network interfaces the - * wrapper script uses to create bridges and veth pairs:

- * - * - *

Example registration for a KVM-namespace backend:

- *
- *   cmk registerExtension id=<ext-uuid> resourcetype=PhysicalNetwork \
- *       resourceid=<phys-uuid> \
- *       details[0].key=hosts                details[0].value=192.168.1.10,192.168.1.11 \
- *       details[1].key=port                 details[1].value=22 \
- *       details[2].key=username             details[2].value=root \
- *       details[3].key=sshkey               details[3].value="$(cat ~/.ssh/id_rsa)" \
- *       details[4].key=guest.network.device details[4].value=eth1 \
- *       details[5].key=public.network.device details[5].value=eth1
- * 
- * - *

Per-network extension details

- * On first {@code implement}, the script is called with - * {@code ensure-network-device}. The script selects a host (e.g. from the - * {@code hosts} list in the physical-network details), checks it is reachable, - * and prints a JSON object to stdout. CloudStack stores this verbatim in - * {@code network_details} under key {@value #NETWORK_DETAIL_EXTENSION_DETAILS} - * and forwards it on every subsequent call via - * {@value #ARG_NETWORK_EXTENSION_DETAILS}. - * - *

Example per-network details (KVM-namespace backend):

- *
{"host":"192.168.1.10","namespace":"cs-net-42"}
- * - *

Network capabilities

- * When creating the extension, set detail {@code network.service.capabilities} to a - * JSON object describing the services and their capabilities: - *
- * {
- *   "services": ["SourceNat", "StaticNat", "PortForwarding", "Firewall"],
- *   "capabilities": {
- *     "SourceNat": { "SupportedSourceNatTypes": "peraccount", "RedundantRouter": "false" }
- *   }
- * }
- * 
- */ public class NetworkExtensionElement extends AdapterBase implements NetworkElement, SourceNatServiceProvider, StaticNatServiceProvider, PortForwardingServiceProvider, IpDeployer, NetworkCustomActionProvider, @@ -283,14 +199,15 @@ public class NetworkExtensionElement extends AdapterBase implements // ---- Script argument names ---- - /** CLI argument carrying physical-network extension details as a JSON object. */ - public static final String ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS = "--physical-network-extension-details"; + 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"; - /** CLI argument carrying per-network opaque JSON blob. */ - public static final String ARG_NETWORK_EXTENSION_DETAILS = "--network-extension-details"; + public static final int DEFAULT_SCRIPT_TIMEOUT_SECONDS = 60; - /** CLI argument carrying per-action parameters as a JSON object. */ - public static final String ARG_ACTION_PARAMS = "--action-params"; + public static final int EXIT_CODE_SUCCESS = 0; + public static final int EXIT_CODE_FAILURE = -1; // ---- Script command names ---- @@ -324,6 +241,7 @@ public class NetworkExtensionElement extends AdapterBase implements 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 ---- @@ -490,21 +408,18 @@ public class NetworkExtensionElement extends AdapterBase implements String vlanId = getVlanId(network); - // Build common vpc/network args - List vpcArgs = getVpcIdArgs(network); - // Step 2: Create the network on the device. - List implArgs = new ArrayList<>(); - implArgs.add("--network-id"); implArgs.add(String.valueOf(network.getId())); - implArgs.add("--vlan"); implArgs.add(safeStr(vlanId)); - implArgs.add("--gateway"); implArgs.add(safeStr(network.getGateway())); - implArgs.add("--cidr"); implArgs.add(safeStr(network.getCidr())); - implArgs.add("--extension-ip"); implArgs.add(safeStr(extensionIp)); - implArgs.addAll(vpcArgs); + 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 result = executeScriptAndReturnOutput(network, CMD_IMPLEMENT_NETWORK, implArgs.toArray(new String[0])); + Pair result = executeScriptAndReturnOutput(network, CMD_IMPLEMENT_NETWORK, implementPayload); - if (!result.first()) { + if (result.first() != EXIT_CODE_SUCCESS) { return false; } @@ -565,17 +480,6 @@ public class NetworkExtensionElement extends AdapterBase implements return true; } - /** - * Returns {@code ["--nic-uuid", ""]} when the extension so the script - * can use the same UUID when needed. - */ - private List getNicUuidArgs(NicProfile nic) { - if (nic == null || nic.getUuid() == null || nic.getUuid().isBlank()) { - return Collections.emptyList(); - } - return List.of("--nic-uuid", nic.getUuid()); - } - private void applyNicUpdateFromNetwork(Network network, NicProfile nic) { if (nic == null) { return; @@ -605,11 +509,11 @@ public class NetworkExtensionElement extends AdapterBase implements public boolean shutdown(Network network, ReservationContext context, boolean cleanup) throws ConcurrentOperationException, ResourceUnavailableException { logger.info("Shutting down network extension for network {}", network.getId()); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.addAll(getVpcIdArgs(network)); - boolean result = executeScript(network, CMD_SHUTDOWN_NETWORK, args.toArray(new String[0])); + 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-, stored in the extension details. Removing the @@ -627,14 +531,14 @@ public class NetworkExtensionElement extends AdapterBase implements public boolean destroy(Network network, ReservationContext context) throws ConcurrentOperationException, ResourceUnavailableException { logger.info("Destroying network extension for network {}", network.getId()); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.addAll(getVpcIdArgs(network)); + 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, args.toArray(new String[0])); + boolean result = executeScript(network, CMD_DESTROY_NETWORK, payload); if (result) { cleanupPlaceholderNicIp(network, context); networkDetailsDao.removeDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS); @@ -741,39 +645,22 @@ public class NetworkExtensionElement extends AdapterBase implements Extension extension = resolveExtension(network); File scriptFile = resolveScriptFile(network, extension); - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(network.getPhysicalNetworkId(), extension); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add(CMD_ENSURE_NETWORK_DEVICE); - cmdLine.add("--network-id"); - cmdLine.add(String.valueOf(network.getId())); - cmdLine.add("--vlan"); - cmdLine.add(safeStr(getVlanId(network))); - cmdLine.add("--zone-id"); - cmdLine.add(String.valueOf(network.getDataCenterId())); - // Pass VPC ID so the script can derive the correct namespace (cs-net-) - if (network.getVpcId() != null) { - cmdLine.add("--vpc-id"); - cmdLine.add(String.valueOf(network.getVpcId())); - } - cmdLine.add("--current-details"); - cmdLine.add(currentDetails); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(currentDetails); + 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 { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.start(); - String output = new String(process.getInputStream().readAllBytes()).trim(); - int exitCode = process.waitFor(); + Pair result = executeScriptWithFilePayload(scriptFile, + CMD_ENSURE_NETWORK_DEVICE, payload, "Network extension"); + String output = result.second() != null ? result.second() : ""; - if (exitCode != 0) { + if (result.first() != EXIT_CODE_SUCCESS) { logger.warn("ensure-network-device exited {} for network {} — keeping current details", - exitCode, network.getId()); + -1, network.getId()); if ("{}".equals(currentDetails)) { networkDetailsDao.addDetail(network.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); } @@ -864,19 +751,19 @@ public class NetworkExtensionElement extends AdapterBase implements publicCidr = null; } - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(vlanId)); - args.add("--public-ip"); args.add(ip.getAddress().addr()); - args.add("--source-nat"); args.add(String.valueOf(isSourceNat)); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.add("--public-gateway"); args.add(safeStr(publicGateway)); - args.add("--public-cidr"); args.add(safeStr(publicCidr)); - args.add("--public-vlan"); args.add(publicVlanTag); - args.addAll(getVpcIdArgs(network)); + 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, args.toArray(new String[0])); + boolean result = executeScript(network, action, payload); if (!result) { throw new ResourceUnavailableException( "Failed to " + action + " for IP " + ip.getAddress().addr(), @@ -924,22 +811,20 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.info("Applying {} static NAT rules for network {}", rules.size(), config.getId()); String vlanId = getVlanId(config); - List vpcArgs = getVpcIdArgs(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()); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(config.getId())); - args.add("--vlan"); args.add(safeStr(vlanId)); - args.add("--public-ip"); args.add(getIpAddress(rule.getSourceIpAddressId())); - args.add("--public-cidr"); args.add(safeStr(publicCidr)); - args.add("--public-vlan"); args.add(publicVlanTag); - args.add("--private-ip"); args.add(safeStr(rule.getDestIpAddress())); - args.addAll(vpcArgs); - boolean result = executeScript(config, action, args.toArray(new String[0])); + 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()); @@ -961,8 +846,6 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.info("Applying {} port forwarding rules for network {}", rules.size(), network.getId()); String vlanId = getVlanId(network); - List vpcArgs = getVpcIdArgs(network); - for (PortForwardingRule rule : rules) { boolean isRevoke = rule.getState() == FirewallRule.State.Revoke; String action = isRevoke ? CMD_DELETE_PORT_FORWARD : CMD_ADD_PORT_FORWARD; @@ -971,19 +854,19 @@ public class NetworkExtensionElement extends AdapterBase implements String publicCidr = getPublicCidr(rule.getSourceIpAddressId()); String publicVlanTag = getPublicVlanTag(rule.getSourceIpAddressId()); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(vlanId)); - args.add("--public-ip"); args.add(getIpAddress(rule.getSourceIpAddressId())); - args.add("--public-cidr"); args.add(safeStr(publicCidr)); - args.add("--public-vlan"); args.add(publicVlanTag); - args.add("--public-port"); args.add(safeStr(publicPort)); - args.add("--private-ip"); args.add(safeStr(rule.getDestinationIpAddress() != null + 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)); - args.add("--private-port"); args.add(safeStr(privatePort)); - args.add("--protocol"); args.add(safeStr(rule.getProtocol())); - args.addAll(vpcArgs); - boolean result = executeScript(network, action, args.toArray(new String[0])); + 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()); @@ -994,67 +877,15 @@ public class NetworkExtensionElement extends AdapterBase implements // ---- Script execution ---- - /** - * Executes the network-extension.sh script with the given command and arguments. - * - *

Two JSON blobs are always appended as named CLI arguments:

- *
    - *
  • {@value #ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS} {@code } – all - * {@code extension_resource_map_details} for this extension on the physical - * network. Sensitive keys (password, sshkey) are included but redacted in - * log output.
  • - *
  • {@value #ARG_NETWORK_EXTENSION_DETAILS} {@code } – the per-network - * JSON blob from {@code network_details} ({@code {}} if not yet set).
  • - *
- */ - protected boolean executeScript(Network network, String command, String... args) { - return executeScriptAndReturnOutput(network, command, args).first(); + protected boolean executeScript(Network network, String command, JsonObject argsPayload) { + return executeScriptAndReturnOutput(network, command, argsPayload).first() == 0; } - protected Pair executeScriptAndReturnOutput(Network network, String command, String... args) { - Extension extension = resolveExtension(network); - File scriptFile = resolveScriptFile(network, extension); - + protected Pair executeScriptAndReturnOutput(Network network, String command, JsonObject argsPayload) { ensureExtensionDetails(network); - - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(network.getPhysicalNetworkId(), extension); - String networkExtensionDetailsJson = getNetworkExtensionDetailsJson(network); - - // Log the JSON blobs so we can diagnose missing-argument issues in runtime logs - logger.debug("Physical network details JSON: {}", physicalNetworkDetailsJson); - logger.debug("Network extension details JSON: {}", networkExtensionDetailsJson); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add(command); - cmdLine.addAll(Arrays.asList(args)); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(networkExtensionDetailsJson); - - logger.debug("Executing network extension script: {}", String.join(" ", cmdLine)); - - try { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.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 != 0) { - logger.error("Network extension script failed with exit code {}: {}", exitCode, outputStr); - return new Pair<>(false, outputStr); - } - return new Pair<>(true, outputStr); - } catch (Exception e) { - logger.error("Failed to execute network extension script: {}", e.getMessage(), e); - throw new CloudRuntimeException("Failed to execute network extension script", e); - } + Extension extension = resolveExtension(network); + JsonObject payload = buildNetworkScriptPayload(network, argsPayload, extension); + return executeScriptWithFilePayload(network, command, payload); } private JsonObject parseJsonOutput(String outputStr) { @@ -1074,11 +905,28 @@ public class NetworkExtensionElement extends AdapterBase implements } } - private String getJsonString(JsonObject jsonObject, String key) { - if (jsonObject == null || StringUtils.isBlank(key) || !jsonObject.has(key)) { + private String getJsonString(JsonObject jsonObject, String keyPath) { + if (jsonObject == null || StringUtils.isBlank(keyPath)) { return null; } - JsonElement value = jsonObject.get(key); + 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; } @@ -1125,25 +973,53 @@ public class NetworkExtensionElement extends AdapterBase implements } } - /** - * Writes a potentially large payload to a temporary file and passes the file path - * to the extension script via {@code payloadArgName}. This avoids argv size limits - * for multi-MB payloads. - */ - protected boolean executeScriptWithFilePayload(Network network, String command, - String payloadArgName, String payload, String... args) { + protected Pair 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 executeScriptWithFilePayload(File scriptFile, String command, + JsonObject payload, String logPrefix) { File payloadFile = null; try { payloadFile = File.createTempFile("cs-extnet-" + command + "-", ".payload"); - logger.debug("Writing payload {} to payload file {}", payload, payloadFile); - Files.writeString(payloadFile.toPath(), payload != null ? payload : "", StandardCharsets.UTF_8); + 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 cmdArgs = new ArrayList<>(); - cmdArgs.addAll(Arrays.asList(args)); - cmdArgs.add(payloadArgName); - cmdArgs.add(payloadFile.getAbsolutePath()); + List cmdLine = new ArrayList<>(); + cmdLine.add(scriptFile.getAbsolutePath()); + cmdLine.add(command); + cmdLine.add(payloadFile.getAbsolutePath()); + cmdLine.add(String.valueOf(DEFAULT_SCRIPT_TIMEOUT_SECONDS)); - return executeScript(network, command, cmdArgs.toArray(new String[0])); + 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); @@ -1154,6 +1030,27 @@ public class NetworkExtensionElement extends AdapterBase implements } } + 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 ---- /** @@ -1184,56 +1081,82 @@ public class NetworkExtensionElement extends AdapterBase implements return details; } - /** - * Returns {@code ["--vpc-id", ""]} when the network belongs to a VPC, - * or an empty list otherwise. Appended to every script invocation so the - * wrapper script can derive the correct namespace (cs-net-<vpcId>). + * 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 List getVpcIdArgs(Network network) { - if (network.getVpcId() != null) { - return List.of("--vpc-id", String.valueOf(network.getVpcId())); - } - return List.of(); - } - - /** - * Serialises the physical-network extension details to a compact JSON object string. - */ - private String buildPhysicalNetworkDetailsJson(Long physicalNetworkId, Extension extension) { - return mapToJson(buildPhysicalNetworkDetailsMap(physicalNetworkId, extension)); - } - - /** - * Reads the per-network JSON blob from {@code network_details} - * (returns {@code {}} if not yet set). - */ - private String getNetworkExtensionDetailsJson(Network network) { - if (network.getVpcId() != null) { - return getVpcExtensionDetailsJson(network.getVpcId()); - } else { - Map networkDetails = networkDetailsDao.listDetailsKeyPairs(network.getId()); - return networkDetails != null - ? networkDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; - } - } - - - /** - * Serialises a {@code Map} to a compact JSON object string. - * Returns {@code {}} for null or empty maps. - */ - private String mapToJson(Map map) { - if (map == null || map.isEmpty()) { - return "{}"; - } + private JsonObject buildPhysicalNetworkExtensionDetailsPayload(Long physicalNetworkId, Extension extension) { + Map map = buildPhysicalNetworkDetailsMap(physicalNetworkId, extension); JsonObject obj = new JsonObject(); for (Map.Entry entry : map.entrySet()) { if (entry.getValue() != null) { obj.addProperty(entry.getKey(), entry.getValue()); } } - return new Gson().toJson(obj); + 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 vpcDetails = vpcDetailsDao.listDetailsKeyPairs(network.getVpcId()); + json = vpcDetails != null ? vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; + } else { + Map 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 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 parameters) { + JsonObject obj = new JsonObject(); + if (parameters == null || parameters.isEmpty()) { + return obj; + } + for (Map.Entry 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 ---- @@ -1245,53 +1168,27 @@ public class NetworkExtensionElement extends AdapterBase implements /** * Runs a custom action on the external network device. - * Per-action parameters are passed as a JSON object via - * {@value #ARG_ACTION_PARAMS}, e.g.: - *
--action-params '{"key1":"value1","key2":"value2"}'
- * The wrapper script receives the `--action-params` JSON string and forwards - * it unchanged to hook scripts as the `--action-params` CLI argument; hook - * scripts should parse the JSON themselves (for example using `jq` or a - * small shell/awk parser). + * 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 parameters) { Extension extension = resolveExtension(network); File scriptFile = resolveScriptFile(network, extension); - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(network.getPhysicalNetworkId(), extension); - String networkExtensionDetailsJson = getNetworkExtensionDetailsJson(network); - String actionParamsJson = buildActionParamsJson(parameters); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add("custom-action"); - cmdLine.add("--network-id"); - cmdLine.add(String.valueOf(network.getId())); - cmdLine.addAll(getVpcIdArgs(network)); - cmdLine.add("--action"); - cmdLine.add(actionName); - cmdLine.add(ARG_ACTION_PARAMS); - cmdLine.add(actionParamsJson); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(networkExtensionDetailsJson); + 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 { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.start(); - byte[] output = process.getInputStream().readAllBytes(); - int exitCode = process.waitFor(); - String outputStr = new String(output).trim(); + Pair result = executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload, + "Network extension"); + String outputStr = result.second() != null ? result.second().trim() : ""; - logger.debug("Running custom action script: {}", String.join(" ", cmdLine)); - - if (exitCode != 0) { - logger.error("Custom action '{}' failed (exit {}): {}", actionName, exitCode, outputStr); + if (result.first() != EXIT_CODE_SUCCESS) { + logger.error("Custom action '{}' failed: {}", actionName, outputStr); return null; } logger.info("Custom action '{}' completed successfully", actionName); @@ -1309,7 +1206,8 @@ public class NetworkExtensionElement extends AdapterBase implements /** * Runs a custom action on the external network device for a VPC. - * The script receives {@code --vpc-id} (no {@code --network-id}). + * 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 parameters) { @@ -1321,40 +1219,19 @@ public class NetworkExtensionElement extends AdapterBase implements Extension extension = physNetAndExt.second(); File scriptFile = resolveScriptFileForVpc(physicalNetworkId, extension); - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(physicalNetworkId, extension); - String vpcExtDetailsJson = getVpcExtensionDetailsJson(vpc.getId()); - String actionParamsJson = buildActionParamsJson(parameters); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add("custom-action"); - cmdLine.add("--vpc-id"); - cmdLine.add(String.valueOf(vpc.getId())); - cmdLine.add("--action"); - cmdLine.add(actionName); - cmdLine.add(ARG_ACTION_PARAMS); - cmdLine.add(actionParamsJson); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(vpcExtDetailsJson); + 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 { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.start(); - byte[] output = process.getInputStream().readAllBytes(); - int exitCode = process.waitFor(); - String outputStr = new String(output).trim(); + Pair result = executeScriptWithFilePayload(scriptFile, CMD_CUSTOM_ACTION, payload, + "VPC extension"); + String outputStr = result.second() != null ? result.second().trim() : ""; - logger.debug("Running VPC custom action script: {}", String.join(" ", cmdLine)); - - if (exitCode != 0) { - logger.error("VPC custom action '{}' failed (exit {}): {}", actionName, exitCode, outputStr); + if (result.first() != EXIT_CODE_SUCCESS) { + logger.error("VPC custom action '{}' failed: {}", actionName, outputStr); return null; } logger.info("VPC custom action '{}' completed successfully", actionName); @@ -1365,20 +1242,31 @@ public class NetworkExtensionElement extends AdapterBase implements } } - /** - * Serialises custom-action parameters to a compact JSON object string. - * Returns {@code {}} for null or empty maps. - */ - private String buildActionParamsJson(Map parameters) { - if (parameters == null || parameters.isEmpty()) { - return "{}"; - } - JsonObject obj = new JsonObject(); - for (Map.Entry entry : parameters.entrySet()) { - obj.addProperty(entry.getKey(), - entry.getValue() != null ? entry.getValue().toString() : ""); - } - return new Gson().toJson(obj); + private JsonObject buildCustomActionPayload(Network network, Extension extension, String actionName, + Map 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 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 ---- @@ -1490,20 +1378,20 @@ public class NetworkExtensionElement extends AdapterBase implements String extensionIp = ensureExtensionIp(network); logger.debug("addDhcpEntry: network={} mac={} ip={}", network.getId(), nic.getMacAddress(), nic.getIPv4Address()); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--mac"); args.add(safeStr(nic.getMacAddress())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--hostname"); args.add(safeStr(vm.getHostName())); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.add("--dns"); args.add(safeStr(getNetworkDns(network))); - args.add("--default-nic"); args.add(String.valueOf(nic.isDefaultNic())); - args.add("--domain"); args.add(safeStr(network.getNetworkDomain())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_ADD_DHCP_ENTRY, args.toArray(new String[0])); + 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 @@ -1515,16 +1403,17 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.debug("configDhcpSupportForSubnet: network={}", network.getId()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.add("--dns"); args.add(safeStr(getNetworkDns(network))); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.add("--domain"); args.add(safeStr(network.getNetworkDomain())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_CONFIG_DHCP_SUBNET, args.toArray(new String[0])); + 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 @@ -1534,11 +1423,11 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.debug("removeDhcpSupportForSubnet: network={}", network.getId()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_REMOVE_DHCP_SUBNET, args.toArray(new String[0])); + 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 @@ -1562,14 +1451,14 @@ public class NetworkExtensionElement extends AdapterBase implements } json.append("}"); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--nic-id"); args.add(String.valueOf(nicId)); - args.add("--options"); args.add(json.toString()); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getVpcIdArgs(network)); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("nic_id", String.valueOf(nicId)); + payload.addProperty("options", json.toString()); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addVpcIdToPayload(payload, network); try { - return executeScript(network, CMD_SET_DHCP_OPTIONS, args.toArray(new String[0])); + return executeScript(network, CMD_SET_DHCP_OPTIONS, payload); } catch (Exception e) { logger.warn("setExtraDhcpOptions failed for network {}: {}", network.getId(), e.getMessage()); return false; @@ -1585,14 +1474,14 @@ public class NetworkExtensionElement extends AdapterBase implements logger.debug("removeDhcpEntry: network={} mac={} ip={}", network.getId(), nic.getMacAddress(), nic.getIPv4Address()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--mac"); args.add(safeStr(nic.getMacAddress())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_REMOVE_DHCP_ENTRY, args.toArray(new String[0])); + 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("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_REMOVE_DHCP_ENTRY, payload); } // ---- DnsServiceProvider ---- @@ -1608,14 +1497,14 @@ public class NetworkExtensionElement extends AdapterBase implements logger.debug("addDnsEntry: network={} hostname={} ip={}", network.getId(), hostname, nic.getIPv4Address()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--hostname"); args.add(safeStr(hostname)); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_ADD_DNS_ENTRY, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + payload.addProperty("hostname", safeStr(hostname)); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_ADD_DNS_ENTRY, payload); } @Override @@ -1627,16 +1516,17 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.debug("configDnsSupportForSubnet: network={}", network.getId()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.add("--dns"); args.add(safeStr(getNetworkDns(network))); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.add("--domain"); args.add(safeStr(network.getNetworkDomain())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_CONFIG_DNS_SUBNET, args.toArray(new String[0])); + 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_DNS_SUBNET, payload); } @Override @@ -1646,11 +1536,11 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.debug("removeDnsSupportForSubnet: network={}", network.getId()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_REMOVE_DNS_SUBNET, args.toArray(new String[0])); + 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_DNS_SUBNET, payload); } // ---- UserDataServiceProvider ---- @@ -1785,15 +1675,16 @@ public class NetworkExtensionElement extends AdapterBase implements String vmDataArg = Base64.getEncoder().encodeToString( json.toString().getBytes(StandardCharsets.UTF_8)); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nicIpAddress)); - args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); - args.add("--extension-ip"); args.add(safeStr(ensureExtensionIp(network))); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScriptWithFilePayload(network, CMD_SAVE_VM_DATA, "--vm-data-file", - vmDataArg, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nicIpAddress)); + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + payload.addProperty("extension_ip", safeStr(ensureExtensionIp(network))); + payload.addProperty("vm_data", vmDataArg); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + + return executeScript(network, CMD_SAVE_VM_DATA, payload); } @Override @@ -1808,15 +1699,15 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.debug("savePassword: network={} ip={}", network.getId(), nic.getIPv4Address()); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); - args.add("--password"); args.add(password); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_SAVE_PASSWORD, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + payload.addProperty("password", password); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_SAVE_PASSWORD, payload); } @Override @@ -1835,15 +1726,15 @@ public class NetworkExtensionElement extends AdapterBase implements logger.debug("saveUserData: network={} ip={}", network.getId(), nic.getIPv4Address()); // userData is stored as base64; pass it directly so the script can decode it String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); - args.add("--userdata"); args.add(userData); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_SAVE_USERDATA, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + payload.addProperty("userdata", userData); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_SAVE_USERDATA, payload); } @Override @@ -1859,15 +1750,15 @@ public class NetworkExtensionElement extends AdapterBase implements // Encode SSH key as base64 to safely pass via CLI String sshKeyBase64 = Base64.getEncoder().encodeToString(sshPublicKey.getBytes(java.nio.charset.StandardCharsets.UTF_8)); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); - args.add("--sshkey"); args.add(sshKeyBase64); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_SAVE_SSHKEY, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + payload.addProperty("sshkey", sshKeyBase64); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_SAVE_SSHKEY, payload); } @Override @@ -1883,15 +1774,15 @@ public class NetworkExtensionElement extends AdapterBase implements logger.debug("saveHypervisorHostname: network={} ip={} host={}", network.getId(), nic.getIPv4Address(), hostname); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--ip"); args.add(safeStr(nic.getIPv4Address())); - args.add("--gateway"); args.add(safeStr(nic.getIPv4Gateway())); - args.add("--hypervisor-hostname"); args.add(hostname); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.addAll(getNicUuidArgs(nic)); - args.addAll(getVpcIdArgs(network)); - return executeScript(network, CMD_SAVE_HYPERVISOR_HOSTNAME, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("ip", safeStr(nic.getIPv4Address())); + payload.addProperty("gateway", safeStr(nic.getIPv4Gateway())); + payload.addProperty("hypervisor_hostname", hostname); + payload.addProperty("extension_ip", safeStr(extensionIp)); + addNicUuidToPayload(payload, nic); + addVpcIdToPayload(payload, network); + return executeScript(network, CMD_SAVE_HYPERVISOR_HOSTNAME, payload); } // ---- LoadBalancingServiceProvider ---- @@ -1907,7 +1798,6 @@ public class NetworkExtensionElement extends AdapterBase implements } logger.info("Applying {} LB rules for network {}", rules.size(), network.getId()); String vlanId = getVlanId(network); - List vpcArgs = getVpcIdArgs(network); // Serialise all rules as a JSON array and pass as a single --lb-rules argument StringBuilder json = new StringBuilder("["); @@ -1943,12 +1833,12 @@ public class NetworkExtensionElement extends AdapterBase implements } json.append("]"); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(vlanId)); - args.add("--lb-rules"); args.add(json.toString()); - args.addAll(vpcArgs); - boolean result = executeScript(network, CMD_APPLY_LB_RULES, args.toArray(new String[0])); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("vlan", safeStr(vlanId)); + payload.addProperty("lb_rules", json.toString()); + addVpcIdToPayload(payload, network); + boolean result = executeScript(network, CMD_APPLY_LB_RULES, payload); if (!result) { throw new ResourceUnavailableException("Failed to apply LB rules for network " + network.getId(), Network.class, network.getId()); @@ -2127,15 +2017,15 @@ public class NetworkExtensionElement extends AdapterBase implements String rulesBase64 = Base64.getEncoder().encodeToString( json.toString().getBytes(StandardCharsets.UTF_8)); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.addAll(getVpcIdArgs(network)); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("vlan", safeStr(getVlanId(network))); + payload.addProperty("gateway", safeStr(network.getGateway())); + payload.addProperty("cidr", safeStr(network.getCidr())); + payload.addProperty("fw_rules", rulesBase64); + addVpcIdToPayload(payload, network); - boolean result = executeScriptWithFilePayload(network, CMD_APPLY_FW_RULES, "--fw-rules-file", - rulesBase64, args.toArray(new String[0])); + boolean result = executeScript(network, CMD_APPLY_FW_RULES, payload); if (!result) { throw new ResourceUnavailableException( "Failed to apply firewall rules for network " + network.getId(), @@ -2203,18 +2093,18 @@ public class NetworkExtensionElement extends AdapterBase implements String restoreDataBase64 = buildRestoreNetworkData(network, nics, dhcpEnabled, dnsEnabled, userdataEnabled); String extensionIp = ensureExtensionIp(network); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.add("--extension-ip"); args.add(safeStr(extensionIp)); - args.add("--dns"); args.add(safeStr(getNetworkDns(network))); - args.add("--domain"); args.add(safeStr(network.getNetworkDomain())); - args.addAll(getVpcIdArgs(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("vlan", safeStr(getVlanId(network))); + payload.addProperty("extension_ip", safeStr(extensionIp)); + payload.addProperty("dns", safeStr(getNetworkDns(network))); + payload.addProperty("domain", safeStr(network.getNetworkDomain())); + payload.addProperty("restore_data", restoreDataBase64); + addVpcIdToPayload(payload, network); - return executeScriptWithFilePayload(network, CMD_RESTORE_NETWORK, "--restore-data-file", - restoreDataBase64, args.toArray(new String[0])); + return executeScript(network, CMD_RESTORE_NETWORK, payload); } /** @@ -2280,9 +2170,6 @@ public class NetworkExtensionElement extends AdapterBase implements } Long instanceId = nic.getInstanceId(); - if (instanceId == null) { - continue; - } UserVmVO userVm = userVmDao.findById(instanceId); if (userVm == null) { @@ -2475,37 +2362,18 @@ public class NetworkExtensionElement extends AdapterBase implements vpc.getId(), vpc.getZoneId()); return; } - Long physicalNetworkId = physNetAndExt.first(); - Extension extension = physNetAndExt.second(); - File scriptFile = resolveScriptFileForVpc(physicalNetworkId, extension); - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(physicalNetworkId, extension); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add(CMD_ENSURE_NETWORK_DEVICE); - cmdLine.add("--vpc-id"); - cmdLine.add(String.valueOf(vpc.getId())); - cmdLine.add("--zone-id"); - cmdLine.add(String.valueOf(vpc.getZoneId())); - cmdLine.add("--current-details"); - cmdLine.add(currentDetails); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(currentDetails); + JsonObject argsPayload = new JsonObject(); + argsPayload.addProperty("vpc_id", String.valueOf(vpc.getId())); + argsPayload.addProperty("zone_id", String.valueOf(vpc.getZoneId())); + argsPayload.addProperty("current_details", currentDetails); try { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.start(); - String output = new String(process.getInputStream().readAllBytes()).trim(); - int exitCode = process.waitFor(); + Pair result = executeVpcScriptAndReturnOutput(vpc, CMD_ENSURE_NETWORK_DEVICE, argsPayload); + String output = result.second() != null ? result.second() : ""; - logger.debug("Ensuring VPC network device script: {}", String.join(" ", cmdLine)); - - if (exitCode != 0) { + if (result.first() != EXIT_CODE_SUCCESS) { logger.warn("ensure-network-device exited {} for VPC {} — keeping current details", - exitCode, vpc.getId()); + -1, vpc.getId()); if ("{}".equals(currentDetails)) { vpcDetailsDao.addDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS, "{}", false); } @@ -2528,69 +2396,41 @@ public class NetworkExtensionElement extends AdapterBase implements } } - /** - * Returns the per-VPC extension-details JSON from {@code vpc_details} - * (returns {@code {}} if not yet set). - */ - private String getVpcExtensionDetailsJson(long vpcId) { - Map vpcDetails = vpcDetailsDao.listDetailsKeyPairs(vpcId); - return vpcDetails != null - ? vpcDetails.getOrDefault(NETWORK_DETAIL_EXTENSION_DETAILS, "{}") : "{}"; - } - /** * Executes the extension script for a VPC-level command (no tier network required). * Uses VPC-level details from {@code vpc_details}. */ - protected boolean executeVpcScript(Vpc vpc, String command, String... args) { + protected boolean executeVpcScript(Vpc vpc, String command, JsonObject argsPayload) { + return executeVpcScriptAndReturnOutput(vpc, command, argsPayload).first() == EXIT_CODE_SUCCESS; + } + + protected Pair executeVpcScriptAndReturnOutput(Vpc vpc, String command, JsonObject argsPayload) { Pair physNetAndExt = resolveExtensionForVpc(vpc); if (physNetAndExt == null) { logger.warn("executeVpcScript: no extension found for VPC {} zone {}", vpc.getId(), vpc.getZoneId()); - return false; + return new Pair<>(EXIT_CODE_FAILURE, "No extension found for VPC " + vpc.getId()); } Long physicalNetworkId = physNetAndExt.first(); Extension extension = physNetAndExt.second(); File scriptFile = resolveScriptFileForVpc(physicalNetworkId, extension); - - String physicalNetworkDetailsJson = buildPhysicalNetworkDetailsJson(physicalNetworkId, extension); - String vpcExtDetailsJson = getVpcExtensionDetailsJson(vpc.getId()); - - logger.debug("Physical network details JSON: {}", physicalNetworkDetailsJson); - logger.debug("VPC extension details JSON: {}", vpcExtDetailsJson); - - List cmdLine = new ArrayList<>(); - cmdLine.add(scriptFile.getAbsolutePath()); - cmdLine.add(command); - cmdLine.addAll(Arrays.asList(args)); - cmdLine.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS); - cmdLine.add(physicalNetworkDetailsJson); - cmdLine.add(ARG_NETWORK_EXTENSION_DETAILS); - cmdLine.add(vpcExtDetailsJson); - - logger.debug("Executing VPC extension script: {}", String.join(" ", cmdLine)); - try { - ProcessBuilder pb = new ProcessBuilder(cmdLine); - pb.redirectErrorStream(true); - Process process = pb.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 != 0) { - logger.error("VPC extension script {} failed with exit code {}: {}", command, exitCode, outputStr); - return false; - } - return true; + JsonObject payload = buildVpcScriptPayload(vpc, argsPayload, physicalNetworkId, extension); + return executeScriptWithFilePayload(scriptFile, command, payload, "VPC extension"); } catch (Exception e) { logger.error("Failed to execute VPC extension script {}: {}", command, e.getMessage(), e); throw new CloudRuntimeException("Failed to execute VPC extension script: " + command, e); } } + private JsonObject buildVpcScriptPayload(Vpc vpc, JsonObject argsPayload, Long physicalNetworkId, Extension extension) { + JsonObject payload = new JsonObject(); + payload.add(ARG_PHYSICAL_NETWORK_EXTENSION_DETAILS, + buildPhysicalNetworkExtensionDetailsPayload(physicalNetworkId, extension)); + payload.add(ARG_NETWORK_EXTENSION_DETAILS, buildVpcExtensionDetailsPayload(vpc.getId())); + payload.add(ARG_PAYLOAD, argsPayload != null ? argsPayload : new JsonObject()); + return payload; + } + protected PublicIpAddress getVpcSourceNatIp(long vpcId) { final List ips = ipAddressDao.listByAssociatedVpc(vpcId, true); if (ips == null || ips.isEmpty()) { @@ -2634,26 +2474,22 @@ public class NetworkExtensionElement extends AdapterBase implements ensureExtensionDetails(vpc); // Step 2: Create the VPC namespace (no anchor tier network needed). - List implArgs = new ArrayList<>(); - implArgs.add("--vpc-id"); implArgs.add(String.valueOf(vpc.getId())); - implArgs.add("--cidr"); implArgs.add(safeStr(vpc.getCidr())); + JsonObject implPayload = new JsonObject(); + implPayload.addProperty("vpc_id", String.valueOf(vpc.getId())); + implPayload.addProperty("cidr", safeStr(vpc.getCidr())); // Include source NAT IP if already allocated, so the script can set up the // VPC-level SNAT rule for the entire VPC CIDR. final PublicIpAddress sourceNatIp = getVpcSourceNatIp(vpc.getId()); if (sourceNatIp != null) { - implArgs.add("--public-ip"); implArgs.add(safeStr(sourceNatIp.getAddress().addr())); - implArgs.add("--public-vlan"); implArgs.add(safeStr(getPublicVlanTag(sourceNatIp.getId()))); - implArgs.add("--public-gateway"); implArgs.add(safeStr(sourceNatIp.getGateway())); - implArgs.add("--public-cidr"); implArgs.add(safeStr(getPublicCidr(sourceNatIp.getId()))); - implArgs.add("--source-nat"); implArgs.add("true"); + implPayload.addProperty("public_ip", safeStr(sourceNatIp.getAddress().addr())); + implPayload.addProperty("public_vlan", safeStr(getPublicVlanTag(sourceNatIp.getId()))); + implPayload.addProperty("public_gateway", safeStr(sourceNatIp.getGateway())); + implPayload.addProperty("public_cidr", safeStr(getPublicCidr(sourceNatIp.getId()))); + implPayload.addProperty("source_nat", "true"); } - if (!executeVpcScript(vpc, CMD_IMPLEMENT_VPC, implArgs.toArray(new String[0]))) { - return false; - } - - return true; + return executeVpcScript(vpc, CMD_IMPLEMENT_VPC, implPayload); } /** @@ -2677,20 +2513,20 @@ public class NetworkExtensionElement extends AdapterBase implements continue; } - final List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.addAll(getVpcIdArgs(network)); + final JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("vlan", safeStr(getVlanId(network))); + addVpcIdToPayload(payload, network); - final boolean tierResult = executeScript(network, CMD_DESTROY_NETWORK, args.toArray(new String[0])); + final boolean tierResult = executeScript(network, CMD_DESTROY_NETWORK, payload); result = result && tierResult; } } // Remove the VPC namespace and VPC-level details regardless of tier result. - List vpcArgs = new ArrayList<>(); - vpcArgs.add("--vpc-id"); vpcArgs.add(String.valueOf(vpc.getId())); - boolean vpcResult = executeVpcScript(vpc, CMD_SHUTDOWN_VPC, vpcArgs.toArray(new String[0])); + JsonObject vpcPayload = new JsonObject(); + vpcPayload.addProperty("vpc_id", String.valueOf(vpc.getId())); + boolean vpcResult = executeVpcScript(vpc, CMD_SHUTDOWN_VPC, vpcPayload); if (vpcResult) { try { vpcDetailsDao.removeDetail(vpc.getId(), NETWORK_DETAIL_EXTENSION_DETAILS); @@ -2737,17 +2573,17 @@ public class NetworkExtensionElement extends AdapterBase implements return false; } - final List args = new ArrayList<>(); + final JsonObject payload = new JsonObject(); final VlanVO vlan = vlanDao.findById(address.getVlanId()); - args.add("--vpc-id"); args.add(String.valueOf(vpc.getId())); - args.add("--cidr"); args.add(safeStr(vpc.getCidr())); - args.add("--public-ip"); args.add(safeStr(address.getAddress().addr())); - args.add("--public-vlan"); args.add(safeStr(getPublicVlanTag(address.getId()))); - args.add("--public-gateway"); args.add(vlan != null ? safeStr(vlan.getVlanGateway()) : ""); - args.add("--public-cidr"); args.add(safeStr(getPublicCidr(address.getId()))); - args.add("--source-nat"); args.add("true"); + payload.addProperty("vpc_id", String.valueOf(vpc.getId())); + payload.addProperty("cidr", safeStr(vpc.getCidr())); + payload.addProperty("public_ip", safeStr(address.getAddress().addr())); + payload.addProperty("public_vlan", safeStr(getPublicVlanTag(address.getId()))); + payload.addProperty("public_gateway", vlan != null ? safeStr(vlan.getVlanGateway()) : ""); + payload.addProperty("public_cidr", safeStr(getPublicCidr(address.getId()))); + payload.addProperty("source_nat", "true"); - final boolean result = executeVpcScript(vpc, CMD_UPDATE_VPC_SOURCE_NAT_IP, args.toArray(new String[0])); + final boolean result = executeVpcScript(vpc, CMD_UPDATE_VPC_SOURCE_NAT_IP, payload); if (!result) { logger.warn("updateVpcSourceNatIp: failed to update source NAT IP for VPC {} to {}", vpc.getId(), address.getAddress().addr()); @@ -2779,15 +2615,15 @@ public class NetworkExtensionElement extends AdapterBase implements String aclRulesBase64 = buildAclRulesBase64(activeRules); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(config.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(config))); - args.add("--gateway"); args.add(safeStr(config.getGateway())); - args.add("--cidr"); args.add(safeStr(config.getCidr())); - args.addAll(getVpcIdArgs(config)); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(config.getId())); + payload.addProperty("vlan", safeStr(getVlanId(config))); + payload.addProperty("gateway", safeStr(config.getGateway())); + payload.addProperty("cidr", safeStr(config.getCidr())); + payload.addProperty("acl_rules", aclRulesBase64); + addVpcIdToPayload(payload, config); - boolean result = executeScriptWithFilePayload(config, CMD_APPLY_NETWORK_ACL, - "--acl-rules-file", aclRulesBase64, args.toArray(new String[0])); + boolean result = executeScript(config, CMD_APPLY_NETWORK_ACL, payload); if (!result) { throw new ResourceUnavailableException( "Failed to apply network ACL rules for network " + config.getId(), @@ -2820,15 +2656,15 @@ public class NetworkExtensionElement extends AdapterBase implements try { String aclRulesBase64 = buildAclRulesBase64(activeRules); - List args = new ArrayList<>(); - args.add("--network-id"); args.add(String.valueOf(network.getId())); - args.add("--vlan"); args.add(safeStr(getVlanId(network))); - args.add("--gateway"); args.add(safeStr(network.getGateway())); - args.add("--cidr"); args.add(safeStr(network.getCidr())); - args.addAll(getVpcIdArgs(network)); + JsonObject payload = new JsonObject(); + payload.addProperty("network_id", String.valueOf(network.getId())); + payload.addProperty("vlan", safeStr(getVlanId(network))); + payload.addProperty("gateway", safeStr(network.getGateway())); + payload.addProperty("cidr", safeStr(network.getCidr())); + payload.addProperty("acl_rules", aclRulesBase64); + addVpcIdToPayload(payload, network); - boolean r = executeScriptWithFilePayload(network, CMD_APPLY_NETWORK_ACL, - "--acl-rules-file", aclRulesBase64, args.toArray(new String[0])); + boolean r = executeScript(network, CMD_APPLY_NETWORK_ACL, payload); result = result && r; } catch (Exception e) { logger.warn("reorderAclRules: failed for network {}: {}", network.getId(), e.getMessage()); diff --git a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md index 5d9a4c36000..2163c628ad0 100644 --- a/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md +++ b/framework/extensions/src/main/java/org/apache/cloudstack/framework/extensions/network/README.md @@ -39,8 +39,8 @@ hosts. Use it as a working example. 1. [Architecture Overview](#architecture-overview) 2. [Script Placement Convention](#script-placement-convention) 3. [CloudStack Setup Steps](#cloudstack-setup-steps) -4. [Always-present CLI Arguments](#always-present-cli-arguments) -5. [Shared Arguments Reference](#shared-arguments-reference) +4. [Always-present payload fields](#always-present-payload-fields) +5. [Shared payload fields](#shared-payload-fields) 6. [Command Reference](#command-reference) - [ensure-network-device](#ensure-network-device) - [implement-network](#implement-network) @@ -81,9 +81,8 @@ hosts. Use it as a working example. ``` CloudStack Management Server │ - │ exec /.sh [args...] - │ --physical-network-extension-details '{...}' - │ --network-extension-details '{...}' + │ exec /.sh + │ payload-file contains JSON for the command invocation ▼ Your Script (Bash / Python / Go / …) │ @@ -181,9 +180,10 @@ cmk registerExtension \ "details[2].key=password" "details[2].value=s3cr3t" ``` -Any key/value pairs you pass here will be forwarded to every script -invocation as `--physical-network-extension-details`. The schema is entirely -yours — CloudStack treats it as opaque. +Any key/value pairs you pass here are stored with the physical-network +registration as extension metadata. The `custom-action` path embeds them +directly into the payload file under `physical-network-extension-details`. +The schema is entirely yours — CloudStack treats it as opaque. ### Step 4 – Enable the Network Service Provider @@ -216,54 +216,60 @@ cmk updateNetworkOffering id= state=Enabled --- -## Always-present CLI Arguments +## Always-present payload fields -Every command invocation appends these two named arguments **after** all -command-specific arguments: +For all standard network / VPC commands, CloudStack now executes the script as: -| Argument | Value | +```text +/.sh +``` + +`payload-file` contains a JSON object with this envelope: + +```json +{ + "physical-network-extension-details": {}, + "network-extension-details": {}, + "payload": {} +} +``` + +| Field | Value | |---|---| -| `--physical-network-extension-details` | JSON object — all key/value pairs registered via `registerExtension` **plus** `physicalnetworkname` (auto-enriched by CloudStack). | -| `--network-extension-details` | JSON object — the per-network opaque blob last written by `ensure-network-device`; `{}` until the first successful call. | +| `physical-network-extension-details` | Physical-network extension metadata registered on the physical network, enriched with `physicalnetworkname`. | +| `network-extension-details` | Additional network or VPC details stored in CloudStack and forwarded as a JSON object. | +| `payload` | Command-specific JSON object for the command being executed. | -Example call line built by CloudStack: +`timeout-seconds` is currently `60`. -``` -/usr/share/cloudstack-management/extensions/my-sdn/my-sdn.sh \ - implement-network \ - --network-id 42 \ - --vlan 100 \ - --gateway 10.0.0.1 \ - --cidr 10.0.0.0/24 \ - --extension-ip 10.0.0.1 \ - --physical-network-extension-details '{"hosts":"192.168.1.10","username":"admin","password":"s3cr3t","physicalnetworkname":"net1"}' \ - --network-extension-details '{"host":"192.168.1.10","device_id":"vrf-42"}' -``` - -> **Security note:** `password` and `sshkey` values are present verbatim in -> `--physical-network-extension-details` but are **redacted** in CloudStack -> log output. Treat them as secrets; do not log them in your script either. +> **Important:** `custom-action` is still the exception in shape. It uses its +> own top-level payload structure and does **not** wrap command-specific fields +> under a nested `payload` object. Use top-level `action-params` for +> command-specific parameters. --- -## Shared Arguments Reference +## Shared payload fields -The following arguments appear in multiple commands. Descriptions apply -everywhere they are used. +The following names appear repeatedly inside the nested `payload` object. -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | CloudStack numeric network ID. | -| `--vpc-id ` | CloudStack numeric VPC ID. Present only for VPC-tier networks. | -| `--vlan ` | Guest VLAN tag (e.g. `100`). Extracted from the broadcast URI. May be empty for flat networks. | -| `--gateway ` | Guest network gateway (e.g. `10.0.0.1`). | -| `--cidr ` | Guest network CIDR (e.g. `10.0.0.0/24`). | -| `--extension-ip ` | The IP the extension device uses on the guest side. Equals `--gateway` when SourceNat/Gateway service is provided; otherwise a dedicated allocated IP from the guest subnet (see [Extension IP](#extension-ip)). | -| `--public-ip ` | A public (floating) IP address. | -| `--public-cidr ` | CIDR of the public IP (e.g. `203.0.113.5/24`). | -| `--public-vlan ` | VLAN tag of the public IP's network segment. | -| `--public-gateway ` | Gateway of the public IP's network segment. | -| `--private-ip ` | A VM's private IP address inside the guest network. | +| `network_id` | CloudStack numeric network ID. | +| `vpc_id` | CloudStack numeric VPC ID. Present for VPC tier networks and VPC-scoped commands. | +| `vlan` | Guest VLAN tag (for example `100`). Extracted from the broadcast URI. May be empty for flat networks. | +| `gateway` | Guest network gateway (for example `10.0.0.1`). | +| `cidr` | Guest network CIDR (for example `10.0.0.0/24`). | +| `extension_ip` | The IP the extension device uses on the guest side. Equals the gateway when SourceNat/Gateway is provided; otherwise it is a dedicated IP from the guest subnet. | +| `public_ip` | A public IP address. | +| `public_cidr` | CIDR of the public IP (for example `203.0.113.5/24`). | +| `public_vlan` | VLAN tag of the public IP segment. | +| `public_gateway` | Gateway of the public IP segment. | +| `private_ip` | A VM's private guest-network IP address. | +| `source_nat` | Stringified boolean (`"true"` / `"false"`) indicating whether the public IP is a source-NAT IP. | +| `nic_uuid` | NIC UUID when the current API path has a `NicProfile` available. | +| `dns` | Comma-separated DNS server list. | +| `domain` | Network domain suffix. | --- @@ -277,19 +283,33 @@ everywhere they are used. **Purpose:** Select (or re-validate) the device/host that will handle this network. Perform failover to another host if the current one is unreachable. The returned JSON is stored in `network_details` under key `extension.details` and -forwarded back as `--network-extension-details` on every future call. +passed back to later `ensure-network-device` calls as `payload.current_details`. -**Arguments:** +**Payload file shape:** -| Argument | Description | +```json +{ + "physical-network-extension-details": {}, + "network-extension-details": {}, + "payload": { + "network_id": "42", + "vlan": "100", + "zone_id": "1", + "vpc_id": "7", + "current_details": "{}" + } +} +``` + +**Payload fields (`payload` object):** + +| Field | Description | |---|---| -| `--network-id ` | Network ID. (omitted for VPC-level calls; `--vpc-id` is used instead) | -| `--vlan ` | Guest VLAN. (network-level calls only) | -| `--zone-id ` | CloudStack zone ID. | -| `--vpc-id ` | VPC ID (optional for network-level calls; sole identifier for VPC-level calls). | -| `--current-details ` | Previously stored per-network blob (`{}` on first call). | -| `--physical-network-extension-details ` | Physical network details. | -| `--network-extension-details ` | Same as `--current-details`. | +| `network_id` | Network ID. Omitted for VPC-level calls. | +| `vlan` | Guest VLAN. Present only for network-level calls. | +| `zone_id` | CloudStack zone ID. | +| `vpc_id` | VPC ID for VPC-level calls, and also present for VPC tier networks. | +| `current_details` | Previously stored `extension.details` JSON string (`{}` on first call). | **Stdout:** A single-line JSON object. CloudStack stores this verbatim. You can put any fields your script needs (host selection, device ID, segment @@ -313,16 +333,16 @@ restart). virtual segment (VRF, namespace, VLAN, …), attach the guest interface, and configure the gateway. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--extension-ip ` | Device's IP on the guest network. | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `extension_ip` | Device IP on the guest network. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -335,13 +355,13 @@ deletion). per-network `extension.details` blob is removed from CloudStack after a successful return. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -353,7 +373,7 @@ return. placeholder NIC IP (if any) and the `extension.details` blob are cleaned up automatically by CloudStack after a successful return. -**Arguments:** Identical to `shutdown-network`. +**Payload fields (`payload` object):** Identical to `shutdown-network`. --- @@ -366,17 +386,17 @@ shared namespace or VRF that all tiers will attach to). If a source-NAT IP is already allocated for the VPC, its details are also included so the script can set up the VPC-level SNAT rule at this stage. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--vpc-id ` | | -| `--cidr ` | VPC supernet CIDR. | -| `--public-ip ` | Source-NAT IP (optional; present only if already allocated). | -| `--public-vlan ` | VLAN of the source-NAT IP (optional). | -| `--public-gateway ` | Gateway of the source-NAT IP segment (optional). | -| `--public-cidr ` | CIDR of the source-NAT IP (optional). | -| `--source-nat ` | Always `true` when public IP args are present. | +| `vpc_id` | VPC ID. | +| `cidr` | VPC supernet CIDR. | +| `public_ip` | Source-NAT IP, when already allocated. | +| `public_vlan` | VLAN of the source-NAT IP, when present. | +| `public_gateway` | Gateway of the source-NAT IP segment, when present. | +| `public_cidr` | CIDR of the source-NAT IP, when present. | +| `source_nat` | `"true"` when the public IP fields are present. | --- @@ -387,11 +407,11 @@ can set up the VPC-level SNAT rule at this stage. **Purpose:** Remove the VPC-level namespace / VRF and all associated state. The `extension.details` blob is removed from CloudStack after a successful return. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--vpc-id ` | | +| `vpc_id` | VPC ID. | --- @@ -402,17 +422,17 @@ The `extension.details` blob is removed from CloudStack after a successful retur **Purpose:** Update the VPC-level SNAT rule to point to the new public IP. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--vpc-id ` | | -| `--cidr ` | VPC supernet CIDR. | -| `--public-ip ` | New source-NAT IP. | -| `--public-vlan ` | VLAN of the new source-NAT IP. | -| `--public-gateway ` | Gateway of the new source-NAT IP segment. | -| `--public-cidr ` | CIDR of the new source-NAT IP. | -| `--source-nat ` | Always `true`. | +| `vpc_id` | VPC ID. | +| `cidr` | VPC supernet CIDR. | +| `public_ip` | New source-NAT IP. | +| `public_vlan` | VLAN of the new source-NAT IP. | +| `public_gateway` | Gateway of the new source-NAT IP segment. | +| `public_cidr` | CIDR of the new source-NAT IP. | +| `source_nat` | Always `"true"`. | --- @@ -426,20 +446,20 @@ network (source NAT, static NAT, PF, LB allocation). entry so the device can receive traffic for this IP. - `release-ip` — detach the public IP; remove routing. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | Guest VLAN. | -| `--public-ip ` | The public IP being assigned/released. | -| `--source-nat ` | `true` if this is the source NAT IP. | -| `--gateway ` | Guest network gateway. | -| `--cidr ` | Guest network CIDR. | -| `--public-gateway ` | Gateway of the public IP's segment. | -| `--public-cidr ` | CIDR of the public IP (e.g. `203.0.113.5/24`). | -| `--public-vlan ` | Public VLAN tag. | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN. | +| `public_ip` | The public IP being assigned or released. | +| `source_nat` | `"true"` if this is the source NAT IP. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `public_gateway` | Gateway of the public IP segment. | +| `public_cidr` | CIDR of the public IP. | +| `public_vlan` | Public VLAN tag. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -451,17 +471,17 @@ network (source NAT, static NAT, PF, LB allocation). **Purpose:** Configure a 1:1 bidirectional NAT mapping between a public IP and a VM private IP. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--public-ip ` | | -| `--public-cidr ` | | -| `--public-vlan ` | | -| `--private-ip ` | VM's private IP (DNAT destination). | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `public_ip` | Public IP. | +| `public_cidr` | Public IP CIDR. | +| `public_vlan` | Public VLAN tag. | +| `private_ip` | VM private IP (DNAT destination). | +| `vpc_id` | Present for VPC tier networks. | --- @@ -473,20 +493,20 @@ and a VM private IP. **Purpose:** Configure a DNAT rule from `public-ip:public-port` to `private-ip:private-port`. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--public-ip ` | | -| `--public-cidr ` | | -| `--public-vlan ` | | -| `--public-port ` | Port range on the public IP, e.g. `22` or `8080-8090`. | -| `--private-ip ` | VM's private IP. | -| `--private-port ` | Destination port range on the VM, e.g. `22`. | -| `--protocol ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `public_ip` | Public IP. | +| `public_cidr` | Public IP CIDR. | +| `public_vlan` | Public VLAN tag. | +| `public_port` | Port range on the public IP, for example `22` or `8080-8090`. | +| `private_ip` | VM private IP. | +| `private_port` | Destination port range on the VM. | +| `protocol` | Protocol such as `tcp` or `udp`. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -498,25 +518,24 @@ APIs, and during network restart). **Purpose:** Rebuild the entire firewall policy for the network from scratch. CloudStack calls this with a *narrow* scope (one IP's ingress rules or egress -rules per call), but the `--fw-rules-file` payload always contains **all** active +rules per call), but the `fw_rules` payload field always contains **all** active rules for the network, so a full rebuild is always safe. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--fw-rules-file ` | Path to a temporary file containing the Base64-encoded JSON firewall payload (see below). | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `fw_rules` | Base64-encoded JSON string containing the firewall payload shown below. | +| `vpc_id` | Present for VPC tier networks. | -> **Note:** The payload is written to a temporary file to avoid shell argument -> length limits for large rule sets. Read the file contents and then -> Base64-decode to obtain the JSON. +> **Note:** The outer command invocation uses a payload file, but `fw_rules` +> itself is a Base64-encoded string inside the nested `payload` object. -**`--fw-rules-file` payload** (read file, decode base64, then parse JSON): +**Decoded `fw_rules` JSON:** ```json { @@ -569,18 +588,18 @@ rules for the network, so a full rebuild is always safe. **Purpose:** Rebuild the entire ACL policy for the VPC tier from scratch. Rules are applied in ascending `number` order. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--acl-rules-file ` | Path to a temporary file containing the Base64-encoded JSON ACL rules array (see below). | -| `--vpc-id ` | (VPC tier; always present) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `acl_rules` | Base64-encoded JSON array of ACL rules shown below. | +| `vpc_id` | VPC ID. Always present for VPC tiers. | -**`--acl-rules-file` payload** (read file, decode base64, then parse JSON): +**Decoded `acl_rules` JSON:** ```json [ @@ -622,31 +641,33 @@ network whose DHCP service is provided by this extension. **Purpose:** Add or remove a static DHCP lease for the VM. -**`add-dhcp-entry` arguments:** +**`add-dhcp-entry` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--mac ` | VM NIC MAC address, e.g. `02:00:00:00:00:01`. | -| `--ip ` | VM's assigned IP. | -| `--hostname ` | VM hostname. | -| `--gateway ` | | -| `--cidr ` | | -| `--dns ` | Comma-separated DNS server IPs, e.g. `8.8.8.8,8.8.4.4`. | -| `--default-nic ` | `true` if this is the VM's default NIC. | -| `--domain ` | Network domain suffix (e.g. `cs.example.com`). | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `mac` | VM NIC MAC address, for example `02:00:00:00:00:01`. | +| `ip` | VM assigned IP. | +| `hostname` | VM hostname. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `default_nic` | Stringified boolean indicating whether this NIC is the default NIC. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | -**`remove-dhcp-entry` arguments:** +**`remove-dhcp-entry` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--mac ` | VM NIC MAC address. | -| `--ip ` | VM's assigned IP. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `mac` | VM NIC MAC address. | +| `ip` | VM assigned IP. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -657,26 +678,27 @@ network whose DHCP service is provided by this extension. **Purpose:** Configure the DHCP scope (pool, gateway, DNS) for a subnet without tying it to a specific VM. -**`config-dhcp-subnet` arguments:** +**`config-dhcp-subnet` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--dns ` | | -| `--vlan ` | | -| `--domain ` | | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `vlan` | Guest VLAN tag. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | -**`remove-dhcp-subnet` arguments:** +**`remove-dhcp-subnet` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -685,15 +707,15 @@ without tying it to a specific VM. **Called:** When extra DHCP options are set on a NIC (`updateNicExtraDhcpOption` API). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--nic-id ` | CloudStack NIC ID. | -| `--options ` | JSON object `{"":"", …}`, e.g. `{"15":"example.com","119":"search.example.com"}`. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `nic_id` | CloudStack NIC ID. | +| `options` | Compact JSON string such as `{"15":"example.com","119":"search.example.com"}`. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -702,15 +724,16 @@ without tying it to a specific VM. **Called:** When a VM NIC is reserved on a network whose DNS service is provided by this extension. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--hostname ` | VM hostname. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `hostname` | VM hostname. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -718,26 +741,27 @@ provided by this extension. **Called:** When a DNS scope is configured or removed for a subnet. -**`config-dns-subnet` arguments:** +**`config-dns-subnet` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--dns ` | | -| `--vlan ` | | -| `--domain ` | | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `dns` | Comma-separated DNS server list. | +| `vlan` | Guest VLAN tag. | +| `domain` | Network domain suffix. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | -**`remove-dns-subnet` arguments:** +**`remove-dns-subnet` payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `extension_ip` | Extension IP. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -749,22 +773,22 @@ password on a network whose UserData service is provided by this extension. **Purpose:** Store the complete cloud-init metadata set (user-data, meta-data/*, password) for the VM so the metadata HTTP server can serve it. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--gateway ` | | -| `--vm-data-file ` | Path to a temporary file containing the Base64-encoded JSON array of metadata entries (see below). | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC on this network. | +| `extension_ip` | Extension IP. | +| `vm_data` | Base64-encoded JSON array shown below. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | -> **Note:** The payload is written to a temporary file to avoid shell argument -> length limits for large user-data blobs. Read the file contents and then -> Base64-decode to obtain the JSON array. +> **Note:** The outer command invocation uses a payload file, but `vm_data` +> itself is a Base64-encoded string inside the nested `payload` object. -**`--vm-data-file` payload** (read file, decode base64, then parse JSON): +**Decoded `vm_data` JSON:** ```json [ @@ -789,16 +813,17 @@ Your metadata HTTP server should serve each entry at: **Called:** When a password reset is requested for a VM (`resetPasswordForVirtualMachine` API). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--gateway ` | | -| `--password ` | Plain-text new password. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `password` | Plain-text new password. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -807,16 +832,17 @@ Your metadata HTTP server should serve each entry at: **Called:** When a VM's user data is updated (`updateVirtualMachine` with `userdata`). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--gateway ` | | -| `--userdata ` | Base64-encoded raw user-data bytes. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `userdata` | Base64-encoded raw user-data bytes. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -825,16 +851,17 @@ Your metadata HTTP server should serve each entry at: **Called:** When an SSH public key is reset for a VM (`resetSSHKeyForVirtualMachine` API). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--gateway ` | | -| `--sshkey ` | Base64-encoded SSH public key (UTF-8 text). Decode to get the key string. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `sshkey` | Base64-encoded SSH public key (UTF-8 text). Decode to get the key string. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -845,16 +872,17 @@ Your metadata HTTP server should serve each entry at: **Purpose:** Store the hypervisor hostname in the metadata so VMs can identify which host they run on (cloud-init `availability-zone` / host detection). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--ip ` | VM's IP. | -| `--gateway ` | | -| `--hypervisor-hostname ` | Hypervisor node hostname. | -| `--extension-ip ` | | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `ip` | VM IP. | +| `gateway` | Gateway of the VM NIC. | +| `hypervisor_hostname` | Hypervisor node hostname. | +| `extension_ip` | Extension IP. | +| `nic_uuid` | NIC UUID, when available. | +| `vpc_id` | Present for VPC tier networks. | --- @@ -867,16 +895,16 @@ change (`createLoadBalancerRule`, `deleteLoadBalancerRule`, **Purpose:** Configure the load balancer on the device: create/update/delete virtual server → backend pool mappings. -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--vlan ` | | -| `--lb-rules ` | JSON array of LB rules (see below). **Not** base64 encoded. | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `vlan` | Guest VLAN tag. | +| `lb_rules` | JSON array string of LB rules shown below. It is **not** Base64-encoded. | +| `vpc_id` | Present for VPC tier networks. | -**`--lb-rules` format:** +**Decoded `lb_rules` JSON:** ```json [ @@ -915,25 +943,24 @@ operation, after all rules (firewall/NAT/LB) have been re-applied. every VM currently on the network in a single call (instead of N per-VM calls). -**Arguments:** +**Payload fields (`payload` object):** -| Argument | Description | +| Field | Description | |---|---| -| `--network-id ` | | -| `--gateway ` | | -| `--cidr ` | | -| `--vlan ` | | -| `--extension-ip ` | | -| `--dns ` | | -| `--domain ` | | -| `--restore-data-file ` | Path to a temporary file containing the Base64-encoded JSON restore payload (see below). | -| `--vpc-id ` | (optional) | +| `network_id` | Network ID. | +| `gateway` | Guest network gateway. | +| `cidr` | Guest network CIDR. | +| `vlan` | Guest VLAN tag. | +| `extension_ip` | Extension IP. | +| `dns` | Comma-separated DNS server list. | +| `domain` | Network domain suffix. | +| `restore_data` | Base64-encoded JSON restore payload shown below. | +| `vpc_id` | Present for VPC tier networks. | -> **Note:** The payload is written to a temporary file to avoid shell argument -> length limits for large networks. Read the file contents and then -> Base64-decode to obtain the JSON. +> **Note:** The outer command invocation uses a payload file, but `restore_data` +> itself is a Base64-encoded string inside the nested `payload` object. -**`--restore-data-file` payload** (read file, decode base64, then parse JSON): +**Decoded `restore_data` JSON:** ```json { @@ -967,26 +994,36 @@ Each `vm_data[].content` is **base64-encoded** (same as in `save-vm-data`). **Purpose:** Allows operators to trigger ad-hoc operations on the device without defining new CloudStack API calls. -**Arguments (network-level):** +CloudStack writes the full custom-action request to a temporary JSON payload +file and passes that file directly to the script. Unlike the other commands, +`custom-action` does **not** use the nested `{ "payload": ... }` envelope; +command-specific inputs are provided in top-level `action-params`. -| Argument | Description | +It still includes the same top-level extension detail objects used elsewhere, +including `physical-network-extension-details` and `network-extension-details`. + +**Top-level payload keys (network-level):** + +| Key | Description | |---|---| -| `--network-id ` | | -| `--vpc-id ` | (optional, for VPC-tier networks) | -| `--action ` | The action name passed by the operator. | -| `--action-params ` | JSON object with arbitrary key/value parameters. | -| `--physical-network-extension-details ` | | -| `--network-extension-details ` | | +| `network_id` | The CloudStack network ID. | +| `vpc_id` | Present when the network belongs to a VPC. | +| `action` | The action name passed by the operator. | +| `action-params` | JSON object with arbitrary key/value parameters. | +| `physical-network-extension-details` | Physical-network extension details JSON. | +| `network-extension-details` | Stored `extension.details` JSON for the network. | -**Arguments (VPC-level):** +**Top-level payload keys (VPC-level):** -| Argument | Description | +| Key | Description | |---|---| -| `--vpc-id ` | | -| `--action ` | The action name passed by the operator. | -| `--action-params ` | JSON object with arbitrary key/value parameters. | -| `--physical-network-extension-details ` | | -| `--network-extension-details ` | | +| `vpc_id` | The CloudStack VPC ID. | +| `action` | The action name passed by the operator. | +| `action-params` | JSON object with arbitrary key/value parameters. | +| `physical-network-extension-details` | Physical-network extension details JSON. | +| `network-extension-details` | Stored `extension.details` JSON for the VPC. | + +Hook scripts should parse the payload file directly. **Stdout:** Returned verbatim to the API caller. @@ -1073,18 +1110,18 @@ Only declare services and capabilities your implementation actually supports. ## VPC Networks -For networks that belong to a VPC, `--vpc-id ` is appended to every -command. Use it to share state across all tiers of the same VPC (e.g., a +For networks that belong to a VPC, `vpc_id` is added to the nested command +payload. Use it to share state across all tiers of the same VPC (e.g., a single VRF or namespace per VPC instead of per tier). -In `ensure-network-device`, use `--vpc-id` (when present) as the hash key for +In `ensure-network-device`, use `vpc_id` (when present) as the hash key for host selection so all tiers of a VPC always land on the same device. VPC lifecycle commands (`implement-vpc`, `shutdown-vpc`, `update-vpc-source-nat-ip`) are invoked at the VPC level with no -`--network-id` — only `--vpc-id`. Tier-level commands such as +`network_id` — only `vpc_id`. Tier-level commands such as `implement-network` and `destroy-network` still receive both -`--network-id` and `--vpc-id`. +`network_id` and `vpc_id`. To use this extension as a VPC provider: @@ -1095,9 +1132,9 @@ To use this extension as a VPC provider: ## Extension IP -`--extension-ip` is the IP the device presents on the guest network side: +`extension_ip` is the IP the device presents on the guest network side: -- **With SourceNat or Gateway service:** equals `--gateway` (the device is the +- **With SourceNat or Gateway service:** equals `gateway` (the device is the gateway; no separate IP needed). - **Without SourceNat/Gateway** (Dhcp/Dns/UserData only, e.g. a shared network helper): CloudStack allocates a dedicated IP from the guest subnet and passes @@ -1134,30 +1171,48 @@ point. Replace each `TODO` block with your device's API calls. # my-sdn.sh — CloudStack NetworkOrchestrator extension entry-point set -euo pipefail -COMMAND="${1:-}"; shift || true +COMMAND="${1:-}" +PAYLOAD_FILE="${2:-}" +TIMEOUT_SECONDS="${3:-60}" -# Parse arguments into an associative array -declare -A ARGS -while [[ $# -gt 0 ]]; do - case "$1" in - --*) ARGS["${1#--}"]="${2:-}"; shift 2 ;; - *) shift ;; - esac -done +if [[ -z "${COMMAND}" || -z "${PAYLOAD_FILE}" ]]; then + echo "Usage: $0 " >&2 + exit 1 +fi -# Helpers -phys() { echo "${ARGS[physical-network-extension-details]:-{}}"; } -netdetail(){ echo "${ARGS[network-extension-details]:-{}}"; } -arg() { echo "${ARGS[$1]:-}"; } +root_field() { + python3 - "$PAYLOAD_FILE" "$1" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as fh: + data = json.load(fh) +value = data.get(sys.argv[2], "") +if isinstance(value, (dict, list)): + print(json.dumps(value, separators=(",", ":"))) +elif value is None: + print("") +else: + print(value) +PY +} -# Read a file-payload argument, base64-decode, and echo the JSON -read_payload() { - local filepath="${ARGS[$1]:-}" - if [[ -z "${filepath}" || ! -f "${filepath}" ]]; then - echo "{}" - return - fi - base64 -d < "${filepath}" +payload_field() { + python3 - "$PAYLOAD_FILE" "$1" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as fh: + data = json.load(fh) +value = data.get('payload', {}).get(sys.argv[2], "") +if isinstance(value, (dict, list)): + print(json.dumps(value, separators=(",", ":"))) +elif value is None: + print("") +else: + print(value) +PY +} + +decode_b64_field() { + python3 -c 'import base64, sys; raw = sys.argv[1].strip(); print(base64.b64decode(raw).decode("utf-8") if raw else "")' \ + "$(payload_field "$1")" } case "${COMMAND}" in @@ -1165,12 +1220,12 @@ case "${COMMAND}" in ensure-network-device) # TODO: check that the device is reachable; select/validate host # Print per-network JSON to stdout (stored by CloudStack) - printf '{"device":"%s"}\n' "$(arg hosts | cut -d, -f1)" + printf '{"device":"%s"}\n' "$(payload_field network_id)" ;; implement-network) # TODO: create virtual segment (VRF / VLAN / namespace / …) - # TODO: configure gateway IP $(arg extension-ip) on $(arg cidr) + # TODO: configure gateway IP $(payload_field extension_ip) on $(payload_field cidr) ;; shutdown-network|destroy-network) @@ -1178,36 +1233,36 @@ case "${COMMAND}" in ;; implement-vpc) - # TODO: create VPC-level namespace / VRF for vpc=$(arg vpc-id) - # Optional source-NAT IP: $(arg public-ip) + # TODO: create VPC-level namespace / VRF for vpc=$(payload_field vpc_id) + # Optional source-NAT IP: $(payload_field public_ip) ;; shutdown-vpc) - # TODO: remove VPC namespace / VRF for vpc=$(arg vpc-id) + # TODO: remove VPC namespace / VRF for vpc=$(payload_field vpc_id) ;; update-vpc-source-nat-ip) - # TODO: update VPC SNAT rule to use new public IP $(arg public-ip) + # TODO: update VPC SNAT rule to use new public IP $(payload_field public_ip) ;; assign-ip) - # TODO: attach public IP $(arg public-ip) to the device + # TODO: attach public IP $(payload_field public_ip) to the device ;; release-ip) - # TODO: remove public IP $(arg public-ip) from the device + # TODO: remove public IP $(payload_field public_ip) from the device ;; add-static-nat) - # TODO: DNAT $(arg public-ip) → $(arg private-ip) + # TODO: DNAT $(payload_field public_ip) → $(payload_field private_ip) ;; delete-static-nat) - # TODO: remove DNAT for $(arg public-ip) + # TODO: remove DNAT for $(payload_field public_ip) ;; add-port-forward) - # TODO: DNAT $(arg public-ip):$(arg public-port) → $(arg private-ip):$(arg private-port) + # TODO: DNAT $(payload_field public_ip):$(payload_field public_port) → $(payload_field private_ip):$(payload_field private_port) ;; delete-port-forward) @@ -1215,23 +1270,21 @@ case "${COMMAND}" in ;; apply-fw-rules) - # Read base64 payload from file, decode, then apply - FW_JSON=$(read_payload fw-rules-file) + FW_JSON=$(decode_b64_field fw_rules) # TODO: parse $FW_JSON and apply to device ;; apply-network-acl) - # Read base64 payload from file, decode, then apply - ACL_JSON=$(read_payload acl-rules-file) + ACL_JSON=$(decode_b64_field acl_rules) # TODO: parse $ACL_JSON and apply to VPC tier ;; add-dhcp-entry) - # TODO: add static lease mac=$(arg mac) ip=$(arg ip) + # TODO: add static lease mac=$(payload_field mac) ip=$(payload_field ip) ;; remove-dhcp-entry) - # TODO: remove static lease for mac=$(arg mac) + # TODO: remove static lease for mac=$(payload_field mac) ;; config-dhcp-subnet|remove-dhcp-subnet) ;; @@ -1239,32 +1292,33 @@ case "${COMMAND}" in set-dhcp-options) ;; add-dns-entry) - # TODO: add A record hostname=$(arg hostname) ip=$(arg ip) + # TODO: add A record hostname=$(payload_field hostname) ip=$(payload_field ip) ;; config-dns-subnet|remove-dns-subnet) ;; save-vm-data) - # Read base64 payload from file, decode, then store metadata for ip=$(arg ip) - VM_DATA_JSON=$(read_payload vm-data-file) + VM_DATA_JSON=$(decode_b64_field vm_data) # TODO: iterate entries and write to metadata store ;; save-password|save-userdata|save-sshkey|save-hypervisor-hostname) ;; apply-lb-rules) - # TODO: parse --lb-rules JSON; configure load balancer + LB_JSON=$(payload_field lb_rules) + # TODO: parse $LB_JSON and configure load balancer ;; restore-network) - # Read base64 payload from file, decode, then rebuild DHCP/DNS/metadata - RESTORE_JSON=$(read_payload restore-data-file) + RESTORE_JSON=$(decode_b64_field restore_data) # TODO: iterate vms and restore leases / DNS / metadata ;; custom-action) - # TODO: handle $(arg action) with params $(arg action-params) - echo "custom action $(arg action) not implemented" + ACTION_NAME=$(root_field action) + ACTION_PARAMS=$(root_field action-params) + # TODO: handle $ACTION_NAME with params $ACTION_PARAMS + echo "custom action ${ACTION_NAME} not implemented" exit 1 ;; @@ -1279,4 +1333,4 @@ exit 0 For a full production implementation see https://github.com/apache/cloudstack-extensions/tree/network-namespace/Network-Namespace: - `network-namespace.sh` — management-server entry-point (SSH proxy). -- `enetwork-namespace-wrapper.sh` — KVM-host wrapper that implements all commands using Linux network namespaces. +- `network-namespace-wrapper.sh` — KVM-host wrapper that implements all commands using Linux network namespaces.