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 <[email protected]> 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}"
