You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cloudstack.apache.org by we...@apache.org on 2026/05/12 15:55:13 UTC
(cloudstack-extensions) branch network-namespace updated (124cf71 -> ac8e96e)
This is an automated email from the ASF dual-hosted git repository.
weizhouapache pushed a change to branch network-namespace
in repository https://gitbox.apache.org/repos/asf/cloudstack-extensions.git
discard 124cf71 Network Namespace: Support JSON payload instead of CLI arguments
new ac8e96e Network Namespace: Support JSON payload instead of CLI arguments
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (124cf71)
\
N -- N -- N refs/heads/network-namespace (ac8e96e)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
Network-Namespace/network-namespace-wrapper.sh | 238 +++++++------------------
1 file changed, 66 insertions(+), 172 deletions(-)
(cloudstack-extensions) 01/01: Network Namespace: Support JSON payload instead of CLI arguments
Posted by we...@apache.org.
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
commit ac8e96e29d5a3f036dab12a0df11836b363c70e4
Author: Wei Zhou <we...@apache.org>
AuthorDate: Tue May 12 17:54:59 2026 +0200
Network Namespace: Support JSON payload instead of CLI arguments
---
Network-Namespace/README.md | 145 ++++++++------
Network-Namespace/network-namespace-wrapper.sh | 265 ++++++++++++-------------
Network-Namespace/network-namespace.sh | 210 ++++++++++----------
3 files changed, 311 insertions(+), 309 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..fc60ed8 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=("$@")
@@ -431,91 +463,46 @@ ensure_jump() {
##############################################################################
parse_args() {
- NETWORK_ID=""
- NAMESPACE=""
- VPC_ID=""
- CHOSEN_ID=""
- VLAN=""
- GATEWAY=""
- CIDR=""
- PUBLIC_IP=""
- PRIVATE_IP=""
- PUBLIC_PORT=""
- PRIVATE_PORT=""
- PROTOCOL=""
- SOURCE_NAT="false"
- PUBLIC_GATEWAY=""
- PUBLIC_CIDR=""
- PUBLIC_VLAN=""
- # --- new fields ---
- MAC=""
- HOSTNAME=""
- DNS_SERVER=""
- NIC_ID=""
- DHCP_OPTIONS_JSON="{}"
- VM_IP=""
- USERDATA=""
- PASSWORD=""
- SSH_KEY=""
- HYPERVISOR_HOSTNAME=""
- LB_RULES_JSON="[]"
- DEFAULT_NIC="true"
- VM_DATA=""
- VM_DATA_FILE=""
- DOMAIN=""
- EXTENSION_IP=""
- RESTORE_DATA=""
- RESTORE_DATA_FILE=""
- FW_RULES_JSON=""
- FW_RULES_FILE=""
- ACL_RULES_JSON=""
- ACL_RULES_FILE=""
-
- 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 ;;
- --vlan) VLAN="$2"; shift 2 ;;
- --gateway) GATEWAY="$2"; shift 2 ;;
- --cidr) CIDR="$2"; shift 2 ;;
- --public-ip) PUBLIC_IP="$2"; shift 2 ;;
- --private-ip) PRIVATE_IP="$2"; shift 2 ;;
- --public-port) PUBLIC_PORT="$2"; shift 2 ;;
- --private-port) PRIVATE_PORT="$2"; shift 2 ;;
- --protocol) PROTOCOL="$2"; shift 2 ;;
- --source-nat) SOURCE_NAT="$2"; shift 2 ;;
- --public-gateway) PUBLIC_GATEWAY="$2"; shift 2 ;;
- --public-cidr) PUBLIC_CIDR="$2"; shift 2 ;;
- --public-vlan) PUBLIC_VLAN="$2"; shift 2 ;;
- --mac) MAC="$2"; shift 2 ;;
- --hostname) HOSTNAME="$2"; shift 2 ;;
- --dns) DNS_SERVER="$2"; shift 2 ;;
- --nic-id) NIC_ID="$2"; shift 2 ;;
- --options) DHCP_OPTIONS_JSON="$2"; shift 2 ;;
- --ip) VM_IP="$2"; shift 2 ;;
- --userdata) USERDATA="$2"; shift 2 ;;
- --password) PASSWORD="$2"; shift 2 ;;
- --sshkey) SSH_KEY="$2"; shift 2 ;;
- --hypervisor-hostname) HYPERVISOR_HOSTNAME="$2"; shift 2 ;;
- --lb-rules) LB_RULES_JSON="$2"; shift 2 ;;
- --default-nic) DEFAULT_NIC="$2"; shift 2 ;;
- --vm-data) VM_DATA="$2"; shift 2 ;;
- --vm-data-file) VM_DATA_FILE="$2"; shift 2 ;;
- --domain) DOMAIN="$2"; shift 2 ;;
- --extension-ip) EXTENSION_IP="$2"; shift 2 ;;
- --restore-data) RESTORE_DATA="$2"; shift 2 ;;
- --restore-data-file) RESTORE_DATA_FILE="$2"; shift 2 ;;
- --fw-rules) FW_RULES_JSON="$2"; shift 2 ;;
- --fw-rules-file) FW_RULES_FILE="$2"; shift 2 ;;
- --acl-rules) ACL_RULES_JSON="$2"; shift 2 ;;
- --acl-rules-file) ACL_RULES_FILE="$2"; shift 2 ;;
- # consumed by _pre_scan_args — skip silently
- --physical-network-extension-details|--network-extension-details)
- shift 2 ;;
- *) shift ;;
- esac
- done
+ 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")
+
+ [ -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"
@@ -2240,10 +2227,10 @@ cmd_save_vm_data() {
log "save-vm-data: network=${NETWORK_ID} ip=${VM_IP}"
[ -z "${VM_IP}" ] && die "save-vm-data: missing --ip"
- local vm_data_file="${VM_DATA_FILE}"
+ local vm_data_file=""
local cleanup_vm_data_file="false"
if [ -z "${vm_data_file}" ]; then
- [ -z "${VM_DATA}" ] && die "save-vm-data: missing --vm-data or --vm-data-file"
+ [ -z "${VM_DATA}" ] && die "save-vm-data: missing payload.vm_data"
vm_data_file=$(mktemp /tmp/cs-extnet-vm-data-XXXXXX)
cleanup_vm_data_file="true"
printf '%s' "${VM_DATA}" > "${vm_data_file}"
@@ -2417,7 +2404,7 @@ cmd_apply_fw_rules() {
acquire_lock "${NETWORK_ID}"
log "apply-fw-rules: network=${NETWORK_ID} ns=${NAMESPACE}"
- local fw_rules_file="${FW_RULES_FILE}"
+ local fw_rules_file=""
local cleanup_fw_rules_file="false"
if [ -z "${fw_rules_file}" ]; then
fw_rules_file=$(mktemp /tmp/cs-extnet-fw-rules-XXXXXX)
@@ -2918,28 +2905,23 @@ raw = os.environ.get("RAW_OUTPUT", "")
rows = [line.rstrip() for line in raw.splitlines() if line.strip()]
if action == "pbr-list-tables":
- message = []
+ data = []
for row in rows:
parts = row.split(None, 1)
if len(parts) == 2 and parts[0].isdigit():
- message.append({"id": parts[0], "name": parts[1]})
+ data.append({"id": parts[0], "name": parts[1]})
else:
- # Preserve raw rows that do not match the standard "<id> <name>" format.
- message.append({"result": row})
+ data.append({"result": row})
+ print(json.dumps({"status": "success", "message": data}))
elif action == "pbr-list-routes":
- message = [{"route": row} for row in rows]
+ data = [{"route": row} for row in rows]
+ print(json.dumps({"status": "success", "message": data}))
elif action == "pbr-list-rules":
- message = [{"rule": row} for row in rows]
-elif rows:
- message = [{"action": action, "result": row} for row in rows]
+ data = [{"rule": row} for row in rows]
+ print(json.dumps({"status": "success", "message": data}))
else:
- message = [{"action": action, "result": "OK"}]
-
-print(json.dumps({
- "status": "success",
- "printmessage": "true",
- "message": message
-}))
+ msg = rows[0] if rows else f"{action}: OK"
+ print(json.dumps({"status": "success", "message": msg}))
PYEOF
}
@@ -2948,8 +2930,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 +2948,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"
@@ -3074,10 +3066,10 @@ cmd_restore_network() {
acquire_lock "${NETWORK_ID}"
log "restore-network: network=${NETWORK_ID} ns=${NAMESPACE}"
- local restore_data_file="${RESTORE_DATA_FILE}"
+ local restore_data_file=""
local cleanup_restore_data_file="false"
if [ -z "${restore_data_file}" ]; then
- [ -z "${RESTORE_DATA}" ] && die "restore-network: missing --restore-data or --restore-data-file"
+ [ -z "${RESTORE_DATA}" ] && die "restore-network: missing payload.restore_data"
restore_data_file=$(mktemp /tmp/cs-extnet-restore-data-XXXXXX)
cleanup_restore_data_file="true"
printf '%s' "${RESTORE_DATA}" > "${restore_data_file}"
@@ -3319,33 +3311,20 @@ PYFLAGSEOF
##############################################################################
parse_vpc_args() {
- VPC_ID=""
- NAMESPACE=""
- VPC_CIDR=""
- PUBLIC_IP=""
- PUBLIC_VLAN=""
- PUBLIC_GATEWAY=""
- PUBLIC_CIDR=""
- SOURCE_NAT="false"
-
- while [ $# -gt 0 ]; do
- case "$1" in
- --vpc-id) VPC_ID="$2"; shift 2 ;;
- --namespace) NAMESPACE="$2"; shift 2 ;;
- --cidr) VPC_CIDR="$2"; shift 2 ;;
- --public-ip) PUBLIC_IP="$2"; shift 2 ;;
- --public-vlan) PUBLIC_VLAN="$2"; shift 2 ;;
- --public-gateway) PUBLIC_GATEWAY="$2"; shift 2 ;;
- --public-cidr) PUBLIC_CIDR="$2"; shift 2 ;;
- --source-nat) SOURCE_NAT="$2"; shift 2 ;;
- # consumed by _pre_scan_args — skip silently
- --physical-network-extension-details|--network-extension-details)
- shift 2 ;;
- *) shift ;;
- esac
- done
+ local payload_file="$1"
+
+ VPC_ID=$(_payload_json_get "${payload_file}" "payload.vpc_id")
+ NAMESPACE=$(_payload_json_get "${payload_file}" "payload.namespace")
+ VPC_CIDR=$(_payload_json_get "${payload_file}" "payload.cidr")
+ PUBLIC_IP=$(_payload_json_get "${payload_file}" "payload.public_ip")
+ PUBLIC_VLAN=$(_payload_json_get "${payload_file}" "payload.public_vlan")
+ PUBLIC_GATEWAY=$(_payload_json_get "${payload_file}" "payload.public_gateway")
+ PUBLIC_CIDR=$(_payload_json_get "${payload_file}" "payload.public_cidr")
+ SOURCE_NAT=$(_payload_json_get "${payload_file}" "payload.source_nat")
+
+ [ -z "${SOURCE_NAT}" ] && SOURCE_NAT="false"
- [ -z "${VPC_ID}" ] && die "Missing --vpc-id"
+ [ -z "${VPC_ID}" ] && die "Missing payload.vpc_id"
if [ -z "${NAMESPACE}" ]; then
local NS_FROM_DETAILS
@@ -3631,8 +3610,8 @@ cmd_destroy_vpc() {
##############################################################################
# Command: apply-network-acl
# Applies VPC network ACL rules to the FORWARD chain inside the namespace.
-# Rules are passed as a Base64-encoded JSON array via --acl-rules-file or
-# --acl-rules. Each rule has:
+# Rules are passed as a Base64-encoded JSON array in payload.acl_rules.
+# Each rule has:
# number, action (allow|deny), trafficType (ingress|egress),
# protocol, portStart, portEnd, icmpType, icmpCode, sourceCidrs[]
##############################################################################
@@ -3643,7 +3622,7 @@ cmd_apply_network_acl() {
acquire_lock "${NETWORK_ID}"
log "apply-network-acl: network=${NETWORK_ID} ns=${NAMESPACE} cidr=${CIDR}"
- local acl_rules_file="${ACL_RULES_FILE}"
+ local acl_rules_file=""
local cleanup_acl_file="false"
if [ -z "${acl_rules_file}" ]; then
acl_rules_file=$(mktemp /tmp/cs-extnet-acl-rules-XXXXXX)
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}"