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