This is an automated email from the ASF dual-hosted git repository.

weizhouapache pushed a commit to branch network-namespace
in repository https://gitbox.apache.org/repos/asf/cloudstack-extensions.git


The following commit(s) were added to refs/heads/network-namespace by this push:
     new 124cf71  Network Namespace: Support JSON payload instead of CLI 
arguments
124cf71 is described below

commit 124cf717af9e52b867206e3e624c6656adc6069d
Author: Wei Zhou <[email protected]>
AuthorDate: Tue May 12 16:27:32 2026 +0200

    Network Namespace: Support JSON payload instead of CLI arguments
---
 Network-Namespace/README.md                    | 145 ++++++++++-------
 Network-Namespace/network-namespace-wrapper.sh | 111 +++++++++++--
 Network-Namespace/network-namespace.sh         | 210 ++++++++++++-------------
 3 files changed, 287 insertions(+), 179 deletions(-)

diff --git a/Network-Namespace/README.md b/Network-Namespace/README.md
index 8b552b1..34b4ba7 100644
--- a/Network-Namespace/README.md
+++ b/Network-Namespace/README.md
@@ -52,7 +52,7 @@ required**.
    - [9. Unregister and delete the 
extension](#9-unregister-and-delete-the-extension)
 6. [Multiple extensions on the same physical 
network](#multiple-extensions-on-the-same-physical-network)
 7. [Wrapper script operations reference](#wrapper-script-operations-reference)
-8. [CLI argument reference](#cli-argument-reference)
+8. [Payload reference](#payload-reference)
 9. [Custom actions](#custom-actions)
 10. [Developer / testing notes](#developer--testing-notes)
 
@@ -173,19 +173,16 @@ physical network.  Both default to `eth1` when not 
explicitly set.
    reads all device details stored in `extension_resource_map_details`.
 3. `NetworkExtensionElement` builds a command line:
    ```
-   <extension_path>/network-namespace.sh <command> --network-id <id> [--vlan 
V] [--gateway G] ...
-       --physical-network-extension-details '<json>'
-       --network-extension-details '<json>'
+   <extension_path>/network-namespace.sh <command> <payload-file> 
<timeout-seconds>
    ```
-   Both JSON blobs are always appended as named CLI arguments:
-   * `--physical-network-extension-details` — JSON object with all 
physical-network
-     registration details (hosts, port, username, sshkey, …)
-   * `--network-extension-details` — per-network JSON blob (selected host, 
namespace, …)
-4. **`network-namespace.sh`** parses those CLI arguments, writes the SSH
+   The payload file includes top-level `physical-network-extension-details`,
+   top-level `network-extension-details`, and command-specific fields under
+   `payload` (except `custom-action`, which is a flat top-level payload).
+4. **`network-namespace.sh`** parses the payload JSON, writes the SSH
    private key to a temporary file (if `sshkey` is set in the physical-network
-   details), then SSHes to the remote host and runs the wrapper script with 
both
-   JSON blobs forwarded as CLI arguments.
-5. **`network-namespace-wrapper.sh`** parses the CLI arguments and executes the
+   details), uploads the payload file to the selected host, then runs the 
wrapper
+   script remotely as `<command> <payload-file> <timeout-seconds>`.
+5. **`network-namespace-wrapper.sh`** parses the payload and executes the
    requested operation using `ip link`, `iptables`, `ip addr`, etc. inside the
    network namespace.
 6. Exit codes from `network-namespace.sh`:
@@ -198,7 +195,7 @@ physical network.  Both default to `eth1` when not 
explicitly set.
 
 ### Authentication priority (network-namespace.sh)
 
-1. `sshkey` field in `--physical-network-extension-details` — PEM key written
+1. `sshkey` field in `physical-network-extension-details` — PEM key written
    to a temp file under `/tmp/.cs-extnet-key-XXXXXX/`, used with `ssh -i`.
    **Preferred** — the temp file is deleted on exit.
 2. `password` field — passed to `sshpass(1)` if available; `sshpass` must be
@@ -212,19 +209,19 @@ on `network-namespace.sh` (locally, **no SSH**).  This 
selects the KVM host for
 network:
 
 1. **Sticky re-validation**: if a host was previously selected (from
-   `--current-details["host"]` or `--network-extension-details["host"]`) *and* 
that
+   `payload.current_details.host` or `network-extension-details.host`) *and* 
that
    host is still in the candidate list *and* still reachable, it is kept.
 2. **Hash-based selection**: for new or failed-over networks a stable 
preferred index
    is computed as `CRC32(<routing-key>) mod len(hosts)` where the routing key 
is
-   `vpc-id` for VPC networks (ensuring all tiers land on the same host) or
-   `network-id` for isolated networks.  Hosts are probed in order starting at 
that
+   `vpc_id` for VPC networks (ensuring all tiers land on the same host) or
+   `network_id` for isolated networks. Hosts are probed in order starting at 
that
    index until one answers.
 3. The result is printed as a single-line JSON object:
    ```json
    {"host":"192.168.1.10","namespace":"cs-net-42"}
    ```
-   CloudStack stores this as `network_extension_details` and forwards it to all
-   subsequent calls as `--network-extension-details`.
+   CloudStack stores this in `network_details.extension.details` and forwards 
it
+   to later calls through top-level `network-extension-details`.
 
 You can override the remote wrapper path for testing:
 ```bash
@@ -724,6 +721,8 @@ CloudStack resolves which extension to call by:
 
 ## Wrapper script operations reference
 
+CloudStack now invokes the wrapper through payload files.
+
 The `network-namespace-wrapper.sh` script runs on the remote KVM device.
 It receives the command as its first positional argument followed by named
 `--option value` pairs.
@@ -751,7 +750,7 @@ network-namespace-wrapper.sh implement-network \
 
 Actions:
 1. Create namespace `cs-vpc-<vpc-id>` (VPC) or `cs-net-<network-id>` 
(isolated).
-2. Resolve `GUEST_ETH` from `guest.network.device` in 
`--physical-network-extension-details`
+2. Resolve `GUEST_ETH` from `guest.network.device` in 
`physical-network-extension-details`
    (defaults to `eth1` when absent).
 3. Create VLAN sub-interface `GUEST_ETH.<vlan>` on the host.
 4. Create host bridge `br<GUEST_ETH>-<vlan>` and attach `GUEST_ETH.<vlan>` to 
it.
@@ -917,7 +916,7 @@ network-namespace-wrapper.sh assign-ip \
 ```
 
 Actions:
-1. Resolve `PUB_ETH` from `public.network.device` in 
`--physical-network-extension-details`
+1. Resolve `PUB_ETH` from `public.network.device` in 
`physical-network-extension-details`
    (defaults to `eth1` when absent).
 2. Create VLAN sub-interface `PUB_ETH.<pvlan>` and bridge 
`br<PUB_ETH>-<pvlan>` on the host.
 3. Create veth pair `vph-<pvlan>-<id>` (host) / `vpn-<pvlan>-<id>` (namespace).
@@ -1259,9 +1258,8 @@ in the Java layer).  Writes files under
 reloads both the **apache2 metadata HTTP service** (port 80) and the
 **VR-compatible password server** (port 8080) inside the namespace.
 
-> `network-namespace.sh` (the management-server proxy) automatically uploads
-> large payloads via SCP to a temporary file on the KVM host and passes
-> `--vm-data-file` to the wrapper instead of inlining the base64 blob.
+> `network-namespace.sh` uploads the single command payload file to the KVM 
host;
+> nested fields like `vm_data` stay inside that payload JSON.
 
 ### `save-userdata` / `save-password` / `save-sshkey` / 
`save-hypervisor-hostname`
 
@@ -1313,10 +1311,26 @@ network-namespace-wrapper.sh restore-network \
 
 ```
 network-namespace-wrapper.sh custom-action \
-    --network-id <id> \
-    --action <action-name>
+    <payload-file> \
+    <timeout-seconds>
 ```
 
+CloudStack now writes the custom-action request to a temporary JSON payload 
file
+and passes that file to the wrapper script. The payload contains the network or
+VPC identifiers, the action name, the caller-supplied action parameters, and
+the extension detail blobs that used to be forwarded as individual CLI flags.
+
+Expected payload keys:
+
+| JSON key | Description |
+|----------|-------------|
+| `network_id` | Network ID for network-level actions |
+| `vpc_id` | VPC ID for VPC-level actions |
+| `action` | Custom action name |
+| `action-params` | Caller-supplied JSON object for the action |
+| `physical_network_extension_details` | Physical-network extension details 
JSON |
+| `network_extension_details` | Per-network / per-VPC extension details JSON |
+
 Built-in actions:
 
 | Action | Description |
@@ -1333,7 +1347,7 @@ Built-in actions:
 | `pbr-delete-rule` | Delete an `ip rule` policy rule mapped to a specific 
routing table inside the namespace |
 | `pbr-list-rules` | List policy rules (or only rules for one table) inside 
the namespace |
 
-PBR action parameter keys (`--action-params` JSON):
+PBR action parameter keys (`action-params` JSON in the payload file):
 
 | Action | Required keys | Optional keys |
 |--------|---------------|---------------|
@@ -1363,16 +1377,28 @@ fails with a descriptive error.
 
 ---
 
-## CLI argument reference
+## Payload reference
+
+### Standard payload envelope
+
+```json
+{
+  "physical-network-extension-details": {},
+  "network-extension-details": {},
+  "payload": {}
+}
+```
+
+For `custom-action`, `payload` is not nested; command fields are top-level.
 
-### JSON blobs always forwarded by `network-namespace.sh`
+### Top-level extension details
 
-| CLI Argument | Description |
+| Top-level key | Description |
 |--------------|-------------|
-| `--physical-network-extension-details <json>` | All 
`extension_resource_map_details` **plus** physical network metadata 
automatically added by `NetworkExtensionElement` (see table below). |
-| `--network-extension-details <json>` | Per-network opaque JSON blob 
(selected host, namespace). |
+| `physical-network-extension-details` | All `extension_resource_map_details` 
**plus** physical network metadata automatically added by 
`NetworkExtensionElement` (see table below). |
+| `network-extension-details` | Per-network opaque JSON blob (selected host, 
namespace). |
 
-### Connection details (keys in `--physical-network-extension-details`)
+### Connection details (keys in `physical-network-extension-details`)
 
 These keys are explicitly set when calling `registerExtension`:
 
@@ -1398,40 +1424,41 @@ The wrapper script uses `guest.network.device` (and 
`public.network.device`) to
 name bridges as `br<eth>-<vlan>` and veth pairs as `vh-<vlan>-<id>` /
 `vn-<vlan>-<id>` (guest) and `vph-<pvlan>-<id>` / `vpn-<pvlan>-<id>` (public).
 
-### Per-network details (keys in `--network-extension-details`)
+### Per-network details (keys in `network-extension-details`)
 
 | JSON key | Description |
 |----------|-------------|
 | `host` | Previously selected host IP (set by `ensure-network-device`) |
 | `namespace` | Linux network namespace name (e.g. `cs-net-<networkId>` or 
`cs-vpc-<vpcId>`) |
 
-### Additional per-command arguments
+### Common keys inside `payload` (standard commands)
 
-| CLI Argument | Commands | Description |
+| `payload` key | Commands | Description |
 |--------------|----------|-------------|
-| `--vpc-id <id>` | all | Present when the network belongs to a VPC; namespace 
becomes `cs-vpc-<vpcId>` |
-| `--public-vlan <pvlan>` | `assign-ip`, `release-ip` | Public IP's VLAN tag 
(e.g. `101`) |
-| `--network-id <id>` | most | Network ID — CHOSEN_ID for veth names is 
`<vpc-id>` when VPC, else `<network-id>` |
-| `--extension-ip <ip>` | `implement-network`, `config-dhcp-subnet`, 
`config-dns-subnet`, `restore-network` | Dedicated IP for DHCP/DNS/metadata 
service (used instead of gateway when the namespace does not own the default 
route) |
-| `--current-details <json>` | `ensure-network-device` (proxy only) | Previous 
`--network-extension-details` JSON; used by `network-namespace.sh` to preserve 
host–namespace affinity across calls |
+| `vpc_id` | many | Present when the network belongs to a VPC; namespace 
becomes `cs-vpc-<vpcId>` |
+| `public_vlan` | `assign-ip`, `release-ip` | Public IP VLAN tag (for example 
`101`) |
+| `network_id` | most | Network ID — CHOSEN_ID for veth names is `<vpc_id>` 
when VPC, else `<network_id>` |
+| `extension_ip` | `implement-network`, `config-dhcp-subnet`, 
`config-dns-subnet`, `restore-network` | Dedicated IP for DHCP/DNS/metadata 
service when it differs from the gateway |
+| `current_details` | `ensure-network-device` | Previous selected-device JSON, 
used to preserve host affinity |
 
 ### Action parameters (custom-action only)
 
-Caller-supplied parameters from `runNetworkCustomAction` are passed as a JSON
-object via the `--action-params` CLI argument:
+Custom-action parameters are embedded in the JSON payload file under
+`action-params`. Hook scripts should read and decode the payload file directly
+instead of expecting individual `--action-params` CLI arguments.
 
-```bash
-network-namespace.sh custom-action \
-    --network-id <id> \
-    --action <name> \
-    --action-params '{"key1":"value1","key2":"value2"}' \
-    --physical-network-extension-details '<json>' \
-    --network-extension-details '<json>'
-```
+Example payload excerpt:
 
-`network-namespace-wrapper.sh` receives `--action-params` and forwards it
-unchanged to hook scripts.  Hook scripts should decode the JSON themselves
-(e.g. using `jq`).
+```json
+{
+  "action": "dump-config",
+  "network_id": "123",
+  "action-params": {
+    "key1": "value1",
+    "key2": "value2"
+  }
+}
+```
 
 ---
 
@@ -1487,17 +1514,13 @@ cmk runNetworkCustomAction networkid=<network-uuid> 
actionid=<pbr-add-rule-id> \
 CloudStack calls `NetworkExtensionElement.runCustomAction()`, which issues:
 ```bash
 network-namespace.sh custom-action \
-    --network-id <id> \
-    --action dump-config \
-    --action-params '{"threshold":"90"}' \
-    --physical-network-extension-details '<json>' \
-    --network-extension-details '<json>'
+    <payload-file> \
+    <timeout-seconds>
 ```
 
 `network-namespace.sh` SSHes to the device and runs 
`network-namespace-wrapper.sh`
-with identical arguments.  The wrapper parses `--action-params` and dispatches
-it to the built-in handler or hook script as the `--action-params` CLI
-argument; hook scripts should parse the JSON argument as needed.
+with the same `<command> <payload-file> <timeout-seconds>` shape. The wrapper
+extracts `action`, `action-params`, and extension-details fields from the 
payload.
 
 ---
 
diff --git a/Network-Namespace/network-namespace-wrapper.sh 
b/Network-Namespace/network-namespace-wrapper.sh
index 8886bd7..75ad8c8 100755
--- a/Network-Namespace/network-namespace-wrapper.sh
+++ b/Network-Namespace/network-namespace-wrapper.sh
@@ -69,11 +69,15 @@
 #   CS_EXTNET_<networkId>_POST – POSTROUTING SNAT chain
 #   CS_EXTNET_FWD_<networkId>  – FORWARD filter chain
 #
-# CLI arguments (forwarded by network-namespace.sh):
-#   --physical-network-extension-details <json>
-#       kvmnetworklabel, public_kvmnetworklabel, hosts, username, …
-#   --network-extension-details <json>
-#       host, namespace, …
+# Invocation (forwarded by network-namespace.sh):
+#   network-namespace-wrapper.sh <command> <payload-file> <timeout-seconds>
+#
+# Standard payload envelope includes top-level:
+#   physical-network-extension-details
+#   network-extension-details
+#   payload               # command-specific keys
+#
+# For custom-action the payload is flat (command-specific keys are top-level).
 ##############################################################################
 
 set -e
@@ -102,6 +106,29 @@ _json_get() {
     printf '%s' "$1" | grep -o "\"$2\":\"[^\"]*\"" | cut -d'"' -f4 || true
 }
 
+_payload_json_get() {
+    # _payload_json_get <payload-file> <dot.path> -> value or compact JSON for 
objects
+    python3 - "$1" "$2" <<'PY'
+import json, sys
+with open(sys.argv[1], encoding='utf-8') as fh:
+    data = json.load(fh)
+cur = data
+for part in sys.argv[2].split('.'):
+    if isinstance(cur, dict):
+        cur = cur.get(part)
+    else:
+        cur = None
+    if cur is None:
+        break
+if cur is None:
+    print("")
+elif isinstance(cur, (dict, list)):
+    print(json.dumps(cur, separators=(",", ":")))
+else:
+    print(str(cur))
+PY
+}
+
 # ---------------------------------------------------------------------------
 # Pre-scan all arguments for the two JSON blobs.
 # ---------------------------------------------------------------------------
@@ -109,6 +136,11 @@ _json_get() {
 PHYS_DETAILS="${CS_PHYSICAL_NETWORK_EXTENSION_DETAILS:-{}}"
 EXTENSION_DETAILS="${CS_NETWORK_EXTENSION_DETAILS:-{}}"
 
+if [ $# -ge 3 ] && [ -f "$2" ]; then
+    PHYS_DETAILS=$(_payload_json_get "$2" "physical-network-extension-details")
+    EXTENSION_DETAILS=$(_payload_json_get "$2" "network-extension-details")
+fi
+
 _pre_scan_args() {
     local i=1
     local args=("$@")
@@ -471,8 +503,45 @@ parse_args() {
     ACL_RULES_JSON=""
     ACL_RULES_FILE=""
 
-    while [ $# -gt 0 ]; do
-        case "$1" in
+    if [ $# -ge 1 ] && [ -f "$1" ]; then
+        local payload_file="$1"
+
+        NETWORK_ID=$(_payload_json_get "${payload_file}" "payload.network_id")
+        NAMESPACE=$(_payload_json_get "${payload_file}" "payload.namespace")
+        VPC_ID=$(_payload_json_get "${payload_file}" "payload.vpc_id")
+        VLAN=$(_payload_json_get "${payload_file}" "payload.vlan")
+        GATEWAY=$(_payload_json_get "${payload_file}" "payload.gateway")
+        CIDR=$(_payload_json_get "${payload_file}" "payload.cidr")
+        PUBLIC_IP=$(_payload_json_get "${payload_file}" "payload.public_ip")
+        PRIVATE_IP=$(_payload_json_get "${payload_file}" "payload.private_ip")
+        PUBLIC_PORT=$(_payload_json_get "${payload_file}" 
"payload.public_port")
+        PRIVATE_PORT=$(_payload_json_get "${payload_file}" 
"payload.private_port")
+        PROTOCOL=$(_payload_json_get "${payload_file}" "payload.protocol")
+        SOURCE_NAT=$(_payload_json_get "${payload_file}" "payload.source_nat")
+        PUBLIC_GATEWAY=$(_payload_json_get "${payload_file}" 
"payload.public_gateway")
+        PUBLIC_CIDR=$(_payload_json_get "${payload_file}" 
"payload.public_cidr")
+        PUBLIC_VLAN=$(_payload_json_get "${payload_file}" 
"payload.public_vlan")
+        MAC=$(_payload_json_get "${payload_file}" "payload.mac")
+        HOSTNAME=$(_payload_json_get "${payload_file}" "payload.hostname")
+        DNS_SERVER=$(_payload_json_get "${payload_file}" "payload.dns")
+        NIC_ID=$(_payload_json_get "${payload_file}" "payload.nic_id")
+        DHCP_OPTIONS_JSON=$(_payload_json_get "${payload_file}" 
"payload.options")
+        VM_IP=$(_payload_json_get "${payload_file}" "payload.ip")
+        USERDATA=$(_payload_json_get "${payload_file}" "payload.userdata")
+        PASSWORD=$(_payload_json_get "${payload_file}" "payload.password")
+        SSH_KEY=$(_payload_json_get "${payload_file}" "payload.sshkey")
+        HYPERVISOR_HOSTNAME=$(_payload_json_get "${payload_file}" 
"payload.hypervisor_hostname")
+        LB_RULES_JSON=$(_payload_json_get "${payload_file}" "payload.lb_rules")
+        DEFAULT_NIC=$(_payload_json_get "${payload_file}" 
"payload.default_nic")
+        VM_DATA=$(_payload_json_get "${payload_file}" "payload.vm_data")
+        DOMAIN=$(_payload_json_get "${payload_file}" "payload.domain")
+        EXTENSION_IP=$(_payload_json_get "${payload_file}" 
"payload.extension_ip")
+        RESTORE_DATA=$(_payload_json_get "${payload_file}" 
"payload.restore_data")
+        FW_RULES_JSON=$(_payload_json_get "${payload_file}" "payload.fw_rules")
+        ACL_RULES_JSON=$(_payload_json_get "${payload_file}" 
"payload.acl_rules")
+    else
+        while [ $# -gt 0 ]; do
+            case "$1" in
             --network-id)          NETWORK_ID="$2";         shift 2 ;;
             --namespace)           NAMESPACE="$2";           shift 2 ;;
             --vpc-id)              VPC_ID="$2";              shift 2 ;;
@@ -514,8 +583,14 @@ parse_args() {
             --physical-network-extension-details|--network-extension-details)
                                    shift 2 ;;
             *)                     shift ;;
-        esac
-    done
+            esac
+        done
+    fi
+
+    [ -z "${SOURCE_NAT}" ] && SOURCE_NAT="false"
+    [ -z "${DHCP_OPTIONS_JSON}" ] && DHCP_OPTIONS_JSON="{}"
+    [ -z "${LB_RULES_JSON}" ] && LB_RULES_JSON="[]"
+    [ -z "${DEFAULT_NIC}" ] && DEFAULT_NIC="true"
 
     [ -z "${NETWORK_ID}" ] && die "Missing --network-id"
 
@@ -2948,8 +3023,17 @@ cmd_custom_action() {
     VPC_ID=""
     ACTION_NAME=""
     ACTION_PARAMS_JSON="{}"
-    while [ $# -gt 0 ]; do
-        case "$1" in
+    if [ $# -ge 1 ] && [ -f "$1" ]; then
+        local payload_file="$1"
+        NETWORK_ID=$(_payload_json_get "${payload_file}" "network_id")
+        VPC_ID=$(_payload_json_get "${payload_file}" "vpc_id")
+        ACTION_NAME=$(_payload_json_get "${payload_file}" "action")
+        ACTION_PARAMS_JSON=$(_payload_json_get "${payload_file}" 
"action-params")
+        [ -z "${ACTION_PARAMS_JSON}" ] && 
ACTION_PARAMS_JSON=$(_payload_json_get "${payload_file}" "action_params")
+        [ -z "${ACTION_PARAMS_JSON}" ] && ACTION_PARAMS_JSON="{}"
+    else
+        while [ $# -gt 0 ]; do
+            case "$1" in
             --network-id)    NETWORK_ID="$2";               shift 2 ;;
             --vpc-id)        VPC_ID="$2";                   shift 2 ;;
             --action)        ACTION_NAME="$2";               shift 2 ;;
@@ -2957,8 +3041,9 @@ cmd_custom_action() {
             --physical-network-extension-details|--network-extension-details)
                              shift 2 ;;
             *)               shift ;;
-        esac
-    done
+            esac
+        done
+    fi
     [ -z "${NETWORK_ID}" ] && [ -z "${VPC_ID}" ] && die "custom-action: 
missing --network-id or --vpc-id"
     [ -z "${ACTION_NAME}" ] && die "custom-action: missing --action"
 
diff --git a/Network-Namespace/network-namespace.sh 
b/Network-Namespace/network-namespace.sh
index 85eb8b2..137f5ee 100755
--- a/Network-Namespace/network-namespace.sh
+++ b/Network-Namespace/network-namespace.sh
@@ -22,44 +22,25 @@
 # Proxy script for the network-namespace CloudStack extension.
 # Runs on the CloudStack management server.
 #
-# Two modes of operation:
+# Invocation model:
+#   network-namespace.sh <command> <payload-file> <timeout-seconds>
 #
-#  1. ensure-network-device  (local, no SSH)
-#     Called by NetworkExtensionElement before every network operation.
-#     Selects or re-validates the network device for the given network ID.
-#     Reads the candidate host list from 
--physical-network-extension-details["hosts"]
-#     (comma-separated).
-#     If the previously selected host (from --current-details JSON) is still
-#     reachable it is kept; otherwise a new host is chosen from the list.
-#     Prints a single-line JSON object to stdout, e.g.:
-#       {"host":"192.168.1.10","namespace":"cs-net-42"}
-#     The caller (NetworkExtensionElement) stores this in network_details and
-#     forwards it to all future calls as --network-extension-details.
-#
-#  2. All other commands  (forwarded to the target host via SSH)
-#     The target host is taken from --network-extension-details["host"].
-#     The remote script (network-namespace-wrapper.sh) is called with all
-#     arguments including both --physical-network-extension-details and
-#     --network-extension-details.
+# The payload JSON includes top-level extension details:
+#   physical-network-extension-details
+#   network-extension-details
 #
-# ---- CLI arguments injected by NetworkExtensionElement ----
+# For standard commands, command-specific keys are nested under payload.{...}.
+# For custom-action, command-specific keys are top-level (flat payload).
 #
-#   --physical-network-extension-details <json>
-#       JSON object with all extension_resource_map_details registered for this
-#       extension on the physical network.  No pre-defined keys — the user and
-#       the script agree on the schema.  Typical keys for a KVM-namespace 
backend:
-#         hosts     – comma-separated list of host IPs for HA/selection
-#         port      – SSH port (default 22)
-#         username  – SSH user (default root)
-#         password  – SSH password  (sensitive, not logged)
-#         sshkey    – PEM-encoded SSH private key  (sensitive, not logged)
+# Two runtime modes:
+#  1) ensure-network-device (local, no SSH): selects/revalidates host and emits
+#     a single-line JSON object like:
+#       {"host":"192.168.1.10","namespace":"cs-net-42"}
+#  2) all other commands: forwards the payload file to the selected host and
+#     executes network-namespace-wrapper.sh remotely.
 #
-#   --network-extension-details <json>
-#       Per-network opaque JSON blob (from network_details key ext.details).
-#       '{}' on the first ensure-network-device call.
-#       This script is the sole owner — CloudStack stores and forwards it 
verbatim.
-#         host      – previously selected host IP
-#         namespace – Linux network namespace name (cs-net-<networkId>)
+# Common extension-detail keys (inside physical-network-extension-details):
+#   hosts, host, port, username, password, sshkey
 #
 # ---- SSH authentication priority ----
 #   1. sshkey  field in --physical-network-extension-details → PEM key
@@ -137,11 +118,46 @@ json_get() {
 # ---------------------------------------------------------------------------
 
 if [ $# -lt 1 ]; then
-    die "Usage: network-namespace.sh <command> [arguments...]" 1
+    die "Usage: network-namespace.sh <command> <payload-file> 
<timeout-seconds>" 1
 fi
 
 COMMAND="$1"
-shift
+shift || true
+
+PAYLOAD_FILE=""
+TIMEOUT_SECONDS=""
+PAYLOAD_MODE="false"
+
+if [ $# -ge 1 ] && [ -f "${1}" ]; then
+    PAYLOAD_FILE="$1"
+    TIMEOUT_SECONDS="${2:-60}"
+    PAYLOAD_MODE="true"
+    shift || true
+    [ $# -gt 0 ] && shift || true
+fi
+
+payload_json_get() {
+    # payload_json_get <file> <path>  where path is dot-separated JSON path
+    python3 - "$1" "$2" <<'PY'
+import json, sys
+with open(sys.argv[1], encoding='utf-8') as fh:
+    data = json.load(fh)
+cur = data
+for part in sys.argv[2].split('.'):
+    if isinstance(cur, dict):
+        cur = cur.get(part)
+    else:
+        cur = None
+    if cur is None:
+        break
+if cur is None:
+    print("")
+elif isinstance(cur, (dict, list)):
+    print(json.dumps(cur, separators=(",", ":")))
+else:
+    print(str(cur))
+PY
+}
 
 # ---------------------------------------------------------------------------
 # Parse CLI arguments: extract known flags, collect the rest as FORWARD_ARGS
@@ -152,48 +168,47 @@ EXTENSION_DETAILS="{}"
 NETWORK_ID=""
 CURRENT_DETAILS="{}"
 VPC_ID=""
-VM_DATA_FILE=""
-FW_RULES_FILE=""
-RESTORE_DATA_FILE=""
-ACL_RULES_FILE=""
 FORWARD_ARGS=()
 
-while [ $# -gt 0 ]; do
-    case "$1" in
-        --physical-network-extension-details)
-            PHYS_DETAILS="${2:-{}}"
-            shift 2 ;;
-        --network-extension-details)
-            EXTENSION_DETAILS="${2:-{}}"
-            shift 2 ;;
-        --network-id)
-            NETWORK_ID="${2:-}"
-            FORWARD_ARGS+=("$1" "$2")
-            shift 2 ;;
-        --vpc-id)
-            VPC_ID="${2:-}"
-            FORWARD_ARGS+=("$1" "$2")
-            shift 2 ;;
-        --current-details)
-            CURRENT_DETAILS="${2:-{}}"
-            shift 2 ;;
-        --vm-data-file)
-            VM_DATA_FILE="${2:-}"
-            shift 2 ;;
-        --fw-rules-file)
-            FW_RULES_FILE="${2:-}"
-            shift 2 ;;
-        --acl-rules-file)
-            ACL_RULES_FILE="${2:-}"
-            shift 2 ;;
-        --restore-data-file)
-            RESTORE_DATA_FILE="${2:-}"
-            shift 2 ;;
-        *)
-            FORWARD_ARGS+=("$1")
-            shift ;;
-    esac
-done
+if [ "${PAYLOAD_MODE}" = "true" ]; then
+    PHYS_DETAILS=$(payload_json_get "${PAYLOAD_FILE}" 
"physical-network-extension-details")
+    EXTENSION_DETAILS=$(payload_json_get "${PAYLOAD_FILE}" 
"network-extension-details")
+
+    if [ "${COMMAND}" = "custom-action" ]; then
+        NETWORK_ID=$(payload_json_get "${PAYLOAD_FILE}" "network_id")
+        VPC_ID=$(payload_json_get "${PAYLOAD_FILE}" "vpc_id")
+    else
+        NETWORK_ID=$(payload_json_get "${PAYLOAD_FILE}" "payload.network_id")
+        VPC_ID=$(payload_json_get "${PAYLOAD_FILE}" "payload.vpc_id")
+        CURRENT_DETAILS=$(payload_json_get "${PAYLOAD_FILE}" 
"payload.current_details")
+        [ -z "${CURRENT_DETAILS}" ] && CURRENT_DETAILS="{}"
+    fi
+else
+    while [ $# -gt 0 ]; do
+        case "$1" in
+            --physical-network-extension-details)
+                PHYS_DETAILS="${2:-{}}"
+                shift 2 ;;
+            --network-extension-details)
+                EXTENSION_DETAILS="${2:-{}}"
+                shift 2 ;;
+            --network-id)
+                NETWORK_ID="${2:-}"
+                FORWARD_ARGS+=("$1" "$2")
+                shift 2 ;;
+            --vpc-id)
+                VPC_ID="${2:-}"
+                FORWARD_ARGS+=("$1" "$2")
+                shift 2 ;;
+            --current-details)
+                CURRENT_DETAILS="${2:-{}}"
+                shift 2 ;;
+            *)
+                FORWARD_ARGS+=("$1")
+                shift ;;
+        esac
+    done
+fi
 
 REMOTE_SCRIPT="${CS_NET_SCRIPT_PATH:-${DEFAULT_SCRIPT_PATH}}"
 
@@ -412,37 +427,22 @@ if [ -z "${REMOTE_HOST}" ]; then
 fi
 [ -z "${REMOTE_HOST}" ] && die "No target host available. Run 
ensure-network-device first." 1
 
-# Build the remote command — quote each argument and forward both JSON blobs
-remote_args=()
-for arg in "${FORWARD_ARGS[@]}"; do
-    remote_args+=("'${arg//"'"/"'\\''"}'" )
-done
-
+# Build and execute remote command
 REMOTE_PAYLOAD_FILES=()
-if [ -n "${VM_DATA_FILE}" ]; then
-    REMOTE_VM_DATA_FILE=$(upload_file_to_remote "${REMOTE_HOST}" 
"${VM_DATA_FILE}" "vm-data")
-    REMOTE_PAYLOAD_FILES+=("${REMOTE_VM_DATA_FILE}")
-    remote_args+=("'--vm-data-file'" "'${REMOTE_VM_DATA_FILE//"'"/"'\\''"}'")
-fi
-if [ -n "${FW_RULES_FILE}" ]; then
-    REMOTE_FW_RULES_FILE=$(upload_file_to_remote "${REMOTE_HOST}" 
"${FW_RULES_FILE}" "fw-rules")
-    REMOTE_PAYLOAD_FILES+=("${REMOTE_FW_RULES_FILE}")
-    remote_args+=("'--fw-rules-file'" "'${REMOTE_FW_RULES_FILE//"'"/"'\\''"}'")
-fi
-if [ -n "${ACL_RULES_FILE}" ]; then
-    REMOTE_ACL_RULES_FILE=$(upload_file_to_remote "${REMOTE_HOST}" 
"${ACL_RULES_FILE}" "acl-rules")
-    REMOTE_PAYLOAD_FILES+=("${REMOTE_ACL_RULES_FILE}")
-    remote_args+=("'--acl-rules-file'" 
"'${REMOTE_ACL_RULES_FILE//"'"/"'\\''"}'")
-fi
-if [ -n "${RESTORE_DATA_FILE}" ]; then
-    REMOTE_RESTORE_DATA_FILE=$(upload_file_to_remote "${REMOTE_HOST}" 
"${RESTORE_DATA_FILE}" "restore-data")
-    REMOTE_PAYLOAD_FILES+=("${REMOTE_RESTORE_DATA_FILE}")
-    remote_args+=("'--restore-data-file'" 
"'${REMOTE_RESTORE_DATA_FILE//"'"/"'\\''"}'")
-fi
+if [ "${PAYLOAD_MODE}" = "true" ]; then
+    REMOTE_PAYLOAD_FILE=$(upload_file_to_remote "${REMOTE_HOST}" 
"${PAYLOAD_FILE}" "payload")
+    REMOTE_PAYLOAD_FILES+=("${REMOTE_PAYLOAD_FILE}")
+    REMOTE_CMD="'${REMOTE_SCRIPT}' '${COMMAND}' 
'${REMOTE_PAYLOAD_FILE//"'"/"'\\''"}' '${TIMEOUT_SECONDS}'"
+else
+    remote_args=()
+    for arg in "${FORWARD_ARGS[@]}"; do
+        remote_args+=("'${arg//"'"/"'\\''"}'" )
+    done
 
-PHYS_ESCAPED="${PHYS_DETAILS//\'/\'\\\'\'}"
-EXT_ESCAPED="${EXTENSION_DETAILS//\'/\'\\\'\'}"
-REMOTE_CMD="'${REMOTE_SCRIPT}' '${COMMAND}' ${remote_args[*]} 
--physical-network-extension-details '${PHYS_ESCAPED}' 
--network-extension-details '${EXT_ESCAPED}'"
+    PHYS_ESCAPED="${PHYS_DETAILS//\'/\'\\\'\'}"
+    EXT_ESCAPED="${EXTENSION_DETAILS//\'/\'\\\'\'}"
+    REMOTE_CMD="'${REMOTE_SCRIPT}' '${COMMAND}' ${remote_args[*]} 
--physical-network-extension-details '${PHYS_ESCAPED}' 
--network-extension-details '${EXT_ESCAPED}'"
+fi
 
 log "Remote: ${REMOTE_USER}@${REMOTE_HOST}:${REMOTE_PORT} cmd=${COMMAND}"
 

Reply via email to