This is an automated email from the ASF dual-hosted git repository.
weizhou 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 668667a Network Namespace: support custom action for VPC
668667a is described below
commit 668667ae1afa77ddaab8579a02422be143334546
Author: Wei Zhou <[email protected]>
AuthorDate: Thu Apr 16 15:42:33 2026 +0200
Network Namespace: support custom action for VPC
---
Network-Namespace/README.md | 280 ++++++++++++++++++-------
Network-Namespace/network-namespace-wrapper.sh | 137 +++++++++++-
2 files changed, 335 insertions(+), 82 deletions(-)
diff --git a/Network-Namespace/README.md b/Network-Namespace/README.md
index 3889941..8b552b1 100644
--- a/Network-Namespace/README.md
+++ b/Network-Namespace/README.md
@@ -188,17 +188,49 @@ physical network. Both default to `eth1` when not
explicitly set.
5. **`network-namespace-wrapper.sh`** parses the CLI arguments and executes the
requested operation using `ip link`, `iptables`, `ip addr`, etc. inside the
network namespace.
-6. Exit code `0` = success; any non-zero exit causes CloudStack to treat the
- operation as failed.
+6. Exit codes from `network-namespace.sh`:
+ * `0` — success
+ * `1` — usage / configuration error (missing arguments, no reachable hosts)
+ * `2` — SSH connection or authentication error
+ * `3` — remote wrapper script returned non-zero
+
+ Any non-zero exit causes CloudStack to treat the operation as failed.
### Authentication priority (network-namespace.sh)
1. `sshkey` field in `--physical-network-extension-details` — PEM key written
- to a temp file, used with `ssh -i`. **Preferred** — the temp file is
deleted
- on exit.
-2. `password` field — passed to `sshpass(1)` if available.
+ 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
+ installed on the management server.
3. Neither set — relies on the SSH agent or host key on the management server.
+### Host selection (`ensure-network-device`)
+
+Before every network operation `NetworkExtensionElement` calls
`ensure-network-device`
+on `network-namespace.sh` (locally, **no SSH**). This selects the KVM host
for the
+network:
+
+1. **Sticky re-validation**: if a host was previously selected (from
+ `--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
+ 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`.
+
+You can override the remote wrapper path for testing:
+```bash
+CS_NET_SCRIPT_PATH=/custom/path/wrapper.sh network-namespace.sh
implement-network ...
+```
+
---
## Installation
@@ -238,9 +270,14 @@ installed in:
* **State:** `/var/lib/cloudstack/<ext-name>/`
(e.g. `/var/lib/cloudstack/network-namespace/`)
-* **Log (wrapper):** `/var/log/cloudstack/extensions/<ext-name>/<ext-name>.log`
+* **Log (wrapper, on KVM host):**
`/var/log/cloudstack/extensions/<ext-name>/<ext-name>.log`
(e.g.
`/var/log/cloudstack/extensions/network-namespace/network-namespace.log`)
* **Log (proxy, on management server):**
`/var/log/cloudstack/extensions/<ext-name>.log`
+ (e.g. `/var/log/cloudstack/extensions/network-namespace.log`)
+
+Additional per-network service logs are also written to the same directory on
the
+KVM host: `dnsmasq-<networkId>.log`, `apache2-<networkId>.log`,
+`passwd-<networkId>.log`.
**Prerequisites on the remote device:**
@@ -422,18 +459,18 @@ cmk createNetwork \
```
When a VM is first deployed into this network, CloudStack calls
-`NetworkExtensionElement.implement()`, which triggers the `implement` command:
+`NetworkExtensionElement.implement()`, which triggers the `implement-network`
command:
```bash
# Management server executes:
-network-namespace.sh implement \
+network-namespace.sh implement-network \
--network-id 42 \
--vlan 100 \
--gateway 10.0.1.1 \
--cidr 10.0.1.0/24
# network-namespace.sh SSHes to the host and runs inside the host:
-network-namespace-wrapper.sh implement \
+network-namespace-wrapper.sh implement-network \
--network-id 42 \
--vlan 100 \
--gateway 10.0.1.1 \
@@ -446,6 +483,17 @@ enables IP forwarding inside the namespace, and creates
per-network iptables
chains: `CS_EXTNET_42_PR` (nat PREROUTING), `CS_EXTNET_42_POST` (nat
POSTROUTING), and `CS_EXTNET_FWD_42` (filter FORWARD).
+> **Note on iptables chains:**
+> | Chain | Table | Purpose |
+> |-------|-------|---------|
+> | `CS_EXTNET_<id>_PR` | `nat` | PREROUTING DNAT (port-forward,
static-NAT) |
+> | `CS_EXTNET_<id>_POST` | `nat` | POSTROUTING SNAT (source-NAT,
static-NAT outbound) |
+> | `CS_EXTNET_FWD_<id>` | `filter` | FORWARD catch-all for this
network |
+> | `CS_EXTNET_FWRULES_<id>` | `filter` | Firewall egress rules (inserted
at pos 1 of FWD chain) |
+> | `CS_EXTNET_FWI_<pubIp>` | `mangle` | Firewall ingress per public IP
(PREROUTING, before DNAT) |
+> | `CS_EXTNET_ACL_<id>` | `filter` | VPC Network ACL (both ingress and
egress; pos 1 of FWD) |
+> | `CS_EXTNET_<vpc-id>_VPC_POST` | `nat` | VPC-level SNAT for entire VPC
CIDR |
+
### 5. Acquire a public IP and enable Source NAT
```bash
@@ -568,23 +616,32 @@ This calls `delete-port-forward` which removes the DNAT
and FORWARD rules.
cmk deleteNetwork id=<network-uuid>
```
-CloudStack calls `shutdown()` (to clean up active state) then `destroy()` (full
-removal). Both commands perform identical cleanup:
+CloudStack calls `shutdown-network` (to clean up active state) then
+`destroy-network` (full removal):
```bash
-network-namespace.sh shutdown --network-id 42 --vlan 100
-network-namespace.sh destroy --network-id 42 --vlan 100
+network-namespace.sh shutdown-network --network-id 42 --vlan 100
+network-namespace.sh destroy-network --network-id 42 --vlan 100
```
-The wrapper:
-1. Removes jump rules from PREROUTING, POSTROUTING, and FORWARD.
-2. Flushes and deletes iptables chains `CS_EXTNET_42_PR`, `CS_EXTNET_42_POST`,
- `CS_EXTNET_FWD_42`, and any `CS_EXTNET_FWRULES_42` / `CS_EXTNET_FWI_*`
chains.
+**`shutdown-network`** wrapper actions:
+1. Removes iptables jump rules and flushes/deletes per-network chains
+ (`CS_EXTNET_42_PR`, `CS_EXTNET_42_POST`, `CS_EXTNET_FWD_42`).
+2. Stops dnsmasq, haproxy, apache2, and password-server processes.
3. Deletes public veth pairs (`vph-<pvlan>-42` / `vpn-<pvlan>-42`) that were
created during `assign-ip` (read from state files).
-4. On `destroy`: also deletes the guest veth host-side (`vh-100-2a`) and
removes
- the namespace `cs-net-42` entirely.
-5. Removes all state under `/var/lib/cloudstack/<ext-name>/network-42/`.
+4. Deletes the guest veth host-side (`vh-100-2a`).
+5. For **isolated** networks: deletes the namespace `cs-net-42`.
+6. For **VPC tier** networks: preserves the shared namespace `cs-vpc-<vpcId>`.
+
+**`destroy-network`** wrapper actions (similar to `shutdown-network`, plus):
+1. Deletes the guest veth host-side (`vh-100-2a`).
+2. Deletes public veth pairs owned by this tier.
+3. Stops per-network services.
+4. Removes per-network state directory
`/var/lib/cloudstack/<ext-name>/network-42/`.
+5. For **isolated** networks: deletes the namespace `cs-net-42`.
+6. For **VPC tier** networks: deregisters this tier from the VPC — namespace is
+ only removed by a subsequent `destroy-vpc` call.
> The host bridge `breth1-100` and VLAN sub-interface `eth1.100` are **not**
> removed — they may still be used by other networks or for VM connectivity.
@@ -678,16 +735,17 @@ All commands:
* Persist state under `/var/lib/cloudstack/<ext-name>/network-<network-id>/`
(or `vpc-<vpc-id>/` for VPC-wide shared state such as public IPs).
-### `implement`
+### `implement-network`
Called when CloudStack activates the network (typically on first VM deploy).
```
-network-namespace-wrapper.sh implement \
+network-namespace-wrapper.sh implement-network \
--network-id <id> \
--vlan <vlan-id> \
--gateway <gateway-ip> \
--cidr <cidr> \
+ [--extension-ip <ext-ip>] \
[--vpc-id <vpc-id>]
```
@@ -698,52 +756,59 @@ Actions:
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.
5. Create veth pair `vh-<vlan>-<id>` (host, in bridge) / `vn-<vlan>-<id>`
(namespace).
-6. Assign `<gateway>/<prefix>` to `vn-<vlan>-<id>` inside the namespace.
-7. Enable IP forwarding inside the namespace.
-8. Create iptables chains `CS_EXTNET_<id>_PR` (nat PREROUTING DNAT),
+6. Assign `<extension-ip>/<prefix>` (or `<gateway>/<prefix>` when
+ `--extension-ip` is not given) to `vn-<vlan>-<id>` inside the namespace.
+ When the extension IP differs from the gateway a default route via the
gateway
+ is also added inside the namespace.
+7. Disable IPv6 inside the namespace (all interfaces).
+8. Enable IP forwarding inside the namespace.
+9. Create iptables chains `CS_EXTNET_<id>_PR` (nat PREROUTING DNAT),
`CS_EXTNET_<id>_POST` (nat POSTROUTING SNAT), and `CS_EXTNET_FWD_<id>`
(filter FORWARD).
-9. Save VLAN, gateway, CIDR, namespace, and network-id / vpc-id to state files.
+10. Save VLAN, gateway, CIDR, extension-ip, and namespace to state files.
-### `shutdown`
+### `shutdown-network`
Called when a network is shut down (may be restarted later).
```
-network-namespace-wrapper.sh shutdown \
+network-namespace-wrapper.sh shutdown-network \
--network-id <id> [--vlan <vlan-id>] [--vpc-id <vpc-id>]
```
Actions:
-1. Stop dnsmasq, haproxy, apache2, and password-server processes running inside
- the namespace (if any).
-2. Flush and remove iptables chains (PREROUTING, POSTROUTING, FORWARD jumps +
- chain contents), including `CS_EXTNET_FWRULES_<id>` and all
`CS_EXTNET_FWI_*`
- ingress chains.
-3. Delete public veth pairs (`vph-<pvlan>-<id>` / `vpn-<pvlan>-<id>`) that were
- created during `assign-ip` (read from state).
-4. Keep namespace and guest veth (`vh-<vlan>-<id>` / `vn-<vlan>-<id>`) intact —
- guest VMs can still connect to `br<GUEST_ETH>-<vlan>`.
-
-### `destroy`
+1. Remove iptables jump rules for this network and flush/delete its chains
+ (`CS_EXTNET_<id>_PR`, `CS_EXTNET_<id>_POST`, `CS_EXTNET_FWD_<id>`).
+2. Delete public veth pairs (`vph-<pvlan>-<id>` / `vpn-<pvlan>-<id>`) that are
+ owned by this tier (guarded by per-IP `.tier` state files).
+3. Delete the guest veth host-side (`vh-<vlan>-<id>`).
+4. Stop dnsmasq, haproxy, apache2, and password-server processes.
+5. For **isolated** networks: delete the namespace `cs-net-<id>`.
+6. For **VPC tier** networks: preserve the shared namespace `cs-vpc-<vpc-id>`.
+
+### `destroy-network`
Called when the network is permanently removed.
```
-network-namespace-wrapper.sh destroy \
+network-namespace-wrapper.sh destroy-network \
--network-id <id> [--vlan <vlan-id>] [--vpc-id <vpc-id>]
```
-Actions (superset of shutdown):
+Actions:
1. Delete guest veth host-side (`vh-<vlan>-<id>`).
-2. Delete public veth pairs (`vph-<pvlan>-<id>` / `vpn-<pvlan>-<id>`).
-3. Delete the namespace (removes all interfaces inside it).
+2. Delete public veth pairs that belong to this tier (guarded by `.tier` state
files).
+3. Stop dnsmasq, haproxy, apache2, and password-server processes.
4. Remove per-network state directory `network-<id>/`.
+5. For **isolated** networks: delete the namespace `cs-net-<id>`.
+6. For **VPC tier** networks: deregister this tier from the VPC tracking
directory
+ (`vpc-<vpc-id>/tiers/<network-id>`) — the namespace is preserved and will be
+ removed by a subsequent `destroy-vpc` call.
> The host bridge `br<GUEST_ETH>-<vlan>` and VLAN sub-interface
> `GUEST_ETH.<vlan>`
> are NOT removed on destroy — they may still be used by other networks or for
> VM connectivity.
-### VPC lifecycle commands: `implement-vpc`, `shutdown-vpc`, `destroy-vpc`
+### VPC lifecycle commands: `implement-vpc`, `update-vpc-source-nat-ip`,
`shutdown-vpc`, `destroy-vpc`
These commands manage VPC-level state. Called by `NetworkExtensionElement` when
implementing, shutting down, or destroying a VPC (before or after per-tier
@@ -754,17 +819,56 @@ network operations).
```
network-namespace-wrapper.sh implement-vpc \
--vpc-id <vpc-id> \
- --cidr <vpc-cidr>
+ [--cidr <vpc-cidr>] \
+ [--public-ip <ip>] [--public-vlan <pvlan>] \
+ [--public-gateway <gw>] [--public-cidr <cidr>] \
+ [--source-nat true|false]
```
Actions:
-1. Create the shared VPC namespace `cs-vpc-<vpc-id>`.
-2. Enable IP forwarding inside the namespace.
-3. Create iptables chains for NAT and filter rules.
-4. Save VPC metadata (CIDR, gateway) to state files under
`/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/`.
+1. Create the shared VPC namespace `cs-vpc-<vpc-id>` (idempotent).
+2. Disable IPv6 and enable IP forwarding inside the namespace.
+3. Optionally, when `--source-nat true`, `--public-ip`, and `--public-vlan`
are all
+ provided and `--cidr` (VPC CIDR) is given:
+ * Create public veth pair `vph-<pvlan>-<vpc-id>` (host) /
`vpn-<pvlan>-<vpc-id>` (namespace).
+ * Assign `<public-ip>` to `vpn-<pvlan>-<vpc-id>` inside the namespace.
+ * Set namespace default route via `--public-gateway` (if given).
+ * Add VPC-level SNAT rule in chain `CS_EXTNET_<vpc-id>_VPC_POST`:
+ all VPC traffic (`<vpc-cidr>`) out `vpn-<pvlan>-<vpc-id>` → `<public-ip>`.
+4. Save VPC namespace name and CIDR to
+ `/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/`.
> This command runs **before** any tier networks are implemented. Tier networks
-> inherit the same namespace and host assignment.
+> inherit the same namespace.
+
+#### `update-vpc-source-nat-ip`
+
+```
+network-namespace-wrapper.sh update-vpc-source-nat-ip \
+ --vpc-id <vpc-id> \
+ --public-ip <new-source-nat-ip> \
+ [--cidr <vpc-cidr>] \
+ [--public-vlan <pvlan>] \
+ [--public-gateway <gw>] \
+ [--public-cidr <cidr>] \
+ [--source-nat true|false]
+```
+
+Actions:
+1. Ensure the target public veth pair exists (`vph-<pvlan>-<vpc-id>` /
`vpn-<pvlan>-<vpc-id>`) and assign the new public IP inside the VPC namespace.
+2. Update host and namespace routes for the new source NAT egress path:
+ * keep host route `<public-ip>/32` via `vph-<pvlan>-<vpc-id>`
+ * replace namespace default route via `--public-gateway` on
`vpn-<pvlan>-<vpc-id>` when provided.
+3. Rebuild VPC SNAT chain `CS_EXTNET_<vpc-id>_VPC_POST` so exactly one SNAT
rule remains:
+ * `-s <vpc-cidr> -o vpn-<pvlan>-<vpc-id> -j SNAT --to-source <public-ip>`.
+4. Reconcile persisted VPC IP markers under
+ `/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/ips/`:
+ * set the new source NAT IP file to `true`
+ * set all other VPC public IP marker files to `false`
+ * persist/update `<ip>.pvlan` for the new source NAT IP.
+
+> This command is used by `NetworkExtensionElement.updateVpcSourceNatIp()` when
+> `updateVPC` is called with `sourcenatipaddress`; it avoids full VPC restart.
#### `shutdown-vpc`
@@ -774,11 +878,12 @@ network-namespace-wrapper.sh shutdown-vpc \
```
Actions:
-1. Flush all iptables rules (ingress, egress, NAT chains inside the namespace).
-2. Stop all services (dnsmasq, haproxy, apache2, password-server) for all
tiers.
-3. Keep the namespace and tier veths intact (tiers may restart).
+1. Delete the VPC namespace `cs-vpc-<vpc-id>` (which removes all interfaces
+ inside it, including per-tier veth pairs).
-> Called when the VPC is shut down; tier networks may be restarted later.
+> Called after all tier networks have been shut down. The namespace itself is
the
+> only resource removed — any host-side bridges and VLAN sub-interfaces are
left
+> intact.
#### `destroy-vpc`
@@ -788,10 +893,10 @@ network-namespace-wrapper.sh destroy-vpc \
```
Actions:
-1. Remove the entire namespace `cs-vpc-<vpc-id>` (deletes all interfaces
inside).
+1. Delete the VPC namespace `cs-vpc-<vpc-id>` (if it still exists).
2. Remove VPC-wide state directory
`/var/lib/cloudstack/<ext-name>/vpc-<vpc-id>/`.
-> This is the final cleanup step; after this, the VPC namespace is gone.
+> This is the final cleanup step; after this, all VPC namespace state is gone.
### `assign-ip`
@@ -820,14 +925,19 @@ Actions:
4. Assign `<public-ip>/32` (or `/<prefix>` if `--public-cidr` given) to
`vpn-<pvlan>-<id>` inside the namespace.
5. Add host route `<public-ip>/32 dev vph-<pvlan>-<id>` so the host can reach
it.
-6. If `--public-gateway` is given, set/replace namespace default route via
+6. Send a gratuitous ARP (`arping -U`) from `vpn-<pvlan>-<id>` to flush stale
ARP
+ entries in the upstream gateway (requires `arping` installed on the KVM
host;
+ skipped silently when not available).
+7. If `--public-gateway` is given, set/replace namespace default route via
`vpn-<pvlan>-<id>`.
-7. If `--source-nat true`:
+8. If `--source-nat true` (and `--vpc-id` is **not** set):
* SNAT rule: `<guest-cidr>` out `vpn-<pvlan>-<id>` → `<public-ip>`
(POSTROUTING chain `CS_EXTNET_<id>_POST`).
* FORWARD ACCEPT for `<guest-cidr>` towards `vpn-<pvlan>-<id>`.
-8. Save public VLAN to state file `ips/<public-ip>.pvlan` (used by
`add-static-nat`,
- `add-port-forward`, `release-ip`).
+ * For VPC tiers (`--vpc-id` present), SNAT is managed by `implement-vpc` —
+ `assign-ip` skips the SNAT rules.
+9. Save public VLAN to state file `ips/<public-ip>.pvlan` and owning tier to
+ `ips/<public-ip>.tier` (used by `add-static-nat`, `add-port-forward`,
`release-ip`).
### `release-ip`
@@ -940,7 +1050,7 @@ Called when CloudStack applies or removes firewall rules
for the network.
network-namespace-wrapper.sh apply-fw-rules \
--network-id <id> \
--vlan <vlan-id> \
- --fw-rules <base64-json> \
+ { --fw-rules <base64-json> | --fw-rules-file <path-on-kvm-host> } \
[--vpc-id <vpc-id>]
```
@@ -987,7 +1097,7 @@ Apply Network ACL (Access Control List) rules for VPC
networks.
network-namespace-wrapper.sh apply-network-acl \
--network-id <id> \
--vlan <vlan-id> \
- --acl-rules <base64-json> \
+ { --acl-rules <base64-json> | --acl-rules-file <path-on-kvm-host> } \
[--vpc-id <vpc-id>]
```
@@ -1017,16 +1127,18 @@ The `--acl-rules` value is a Base64-encoded JSON array
of ACL rule objects:
iptables design:
-* **Ingress rules** (filter FORWARD, chain `CS_EXTNET_ACL_IN_<networkId>`):
- Matches `-i vn-<vlan>-<id>` (traffic entering the VM namespace),
- ordered by rule number. Actions: ACCEPT or DROP.
-
-* **Egress rules** (filter FORWARD, chain `CS_EXTNET_ACL_OUT_<networkId>`):
- Matches `-o vn-<vlan>-<id>` (traffic leaving the VM namespace),
- ordered by rule number. Actions: ACCEPT or DROP.
-
-Both chains are inserted at position 1 of `CS_EXTNET_FWD_<networkId>` so ACL
rules
-take precedence over the catch-all ACCEPT rules.
+* A single **filter FORWARD** chain `CS_EXTNET_ACL_<networkId>` handles both
+ ingress and egress traffic. It is inserted at position 1 of
+ `CS_EXTNET_FWD_<networkId>` so ACL rules take precedence over catch-all
ACCEPT
+ rules.
+* `RELATED,ESTABLISHED` traffic is always accepted first (so active sessions
are
+ not interrupted).
+* Rules are applied in ascending `number` order.
+* **Ingress rules** (`trafficType: Ingress`) match `-o vn-<vlan>-<id>` (traffic
+ going *into* the VM subnet, optionally filtered by `-d <tier-cidr>`).
+* **Egress rules** (`trafficType: Egress`) match `-i vn-<vlan>-<id>` (traffic
+ *from* the VM subnet, with `sourceCidrs` used as destination filter `-d`).
+* A terminal DROP rule at the end of the chain enforces the implicit deny
policy.
### `config-dhcp-subnet` / `remove-dhcp-subnet`
@@ -1137,15 +1249,20 @@ Called on network restart and VM deploy.
network-namespace-wrapper.sh save-vm-data \
--network-id <id> \
--ip <vm-ip> \
- --vm-data <base64-json>
+ { --vm-data <base64-json> | --vm-data-file <path-on-kvm-host> }
```
-The `--vm-data` value is a Base64-encoded JSON array of `{dir, file, content}`
-entries (same format as `generateVmData()` in the Java layer). Writes files
-under `${STATE_DIR}/network-<id>/metadata/<vm-ip>/latest/`. After writing,
-starts or reloads both the **apache2 metadata HTTP service** (port 80) and the
+The `--vm-data` value (or the contents of `--vm-data-file`) is a Base64-encoded
+JSON array of `{dir, file, content}` entries (same format as `generateVmData()`
+in the Java layer). Writes files under
+`${STATE_DIR}/network-<id>/metadata/<vm-ip>/latest/`. After writing, starts or
+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.
+
### `save-userdata` / `save-password` / `save-sshkey` /
`save-hypervisor-hostname`
Granular variants that write individual VM metadata fields:
@@ -1187,7 +1304,7 @@ per-VM calls.
```
network-namespace-wrapper.sh restore-network \
--network-id <id> \
- --restore-data <base64-json> \
+ { --restore-data <base64-json> | --restore-data-file <path-on-kvm-host> } \
[--gateway <gw>] [--cidr <cidr>] [--dns <dns>] \
[--domain <dom>] [--extension-ip <ip>] [--vpc-id <vpc-id>]
```
@@ -1295,6 +1412,8 @@ name bridges as `br<eth>-<vlan>` and veth pairs as
`vh-<vlan>-<id>` /
| `--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 |
### Action parameters (custom-action only)
@@ -1393,7 +1512,8 @@ isolated networks. Key differences from isolated
networks:
instead of each network getting its own (`cs-net-<networkId>`).
* **Host affinity**: All tiers of a VPC land on the same KVM host via stable
hash-based
selection using the VPC ID as the routing key.
-* **VPC-level operations**: `implement-vpc`, `shutdown-vpc`, `destroy-vpc`
commands
+* **VPC-level operations**: `implement-vpc`, `update-vpc-source-nat-ip`,
+ `shutdown-vpc`, `destroy-vpc` commands
manage VPC-wide state (namespace creation/teardown).
* **VPC tier operations**: `implement-network`, `shutdown-network`,
`destroy-network`
commands manage per-tier bridges and routes; the namespace is preserved
across
@@ -1424,6 +1544,8 @@ The test covers:
* Full network lifecycle: implement → assign-ip (source NAT) → static NAT →
port forwarding → firewall rules → DHCP/DNS → shutdown / destroy.
* VPC multi-tier networks with shared namespace and automatic host affinity.
+* VPC source NAT IP update flow (`test_09_vpc_source_nat_ip_update`) including
+ source NAT flag flip from old public IP to new public IP.
* NSP state transitions: Disabled → Enabled → Disabled → Deleted.
* Tests `test_04`, `test_05`, `test_06` (DHCP, DNS, LB) require `arping`,
`dnsmasq`, and `haproxy` on the KVM hosts; the test skips them automatically
diff --git a/Network-Namespace/network-namespace-wrapper.sh
b/Network-Namespace/network-namespace-wrapper.sh
index d237de8..223e334 100755
--- a/Network-Namespace/network-namespace-wrapper.sh
+++ b/Network-Namespace/network-namespace-wrapper.sh
@@ -2890,7 +2890,7 @@ cmd_custom_action() {
*) shift ;;
esac
done
- [ -z "${NETWORK_ID}" ] && die "custom-action: missing --network-id"
+ [ -z "${NETWORK_ID}" ] && [ -z "${VPC_ID}" ] && die "custom-action:
missing --network-id or --vpc-id"
[ -z "${ACTION_NAME}" ] && die "custom-action: missing --action"
# Set NAMESPACE/CHOSEN_ID similar to parse_args
@@ -2905,8 +2905,15 @@ cmd_custom_action() {
fi
CHOSEN_ID="${VPC_ID:-${NETWORK_ID}}"
+ # Ensure the namespace exists when running VPC custom actions
+ if [ -n "${VPC_ID}" ] && ! ip netns list 2>/dev/null | grep -q
"^${NAMESPACE}\b"; then
+ log "custom-action: creating namespace ${NAMESPACE} for VPC ${VPC_ID}"
+ ip netns add "${NAMESPACE}" 2>/dev/null || true
+ ip netns exec "${NAMESPACE}" ip link set lo up 2>/dev/null || true
+ fi
+
_load_state
- acquire_lock "${NETWORK_ID}"
+ acquire_lock "${CHOSEN_ID}"
log "custom-action: network=${NETWORK_ID} ns=${NAMESPACE}
action=${ACTION_NAME} params=${ACTION_PARAMS_JSON}"
@@ -3383,6 +3390,129 @@ cmd_implement_vpc() {
log "implement-vpc: done vpc=${VPC_ID} namespace=${NAMESPACE}"
}
+##############################################################################
+# Command: update-vpc-source-nat-ip
+# Updates VPC source NAT egress to a new public IP without restarting tiers.
+# Reconciles public veth/IP state, default route, VPC SNAT iptables chain,
+# and source NAT markers under ${STATE_DIR}/vpc-<vpcId>/ips/.
+##############################################################################
+
+cmd_update_vpc_source_nat_ip() {
+ parse_vpc_args "$@"
+ acquire_lock "vpc-${VPC_ID}"
+
+ [ -z "${PUBLIC_IP}" ] && die "update-vpc-source-nat-ip: missing
--public-ip"
+
+ local vsd="${STATE_DIR}/vpc-${VPC_ID}"
+ mkdir -p "${vsd}/ips"
+
+ # Load persisted values when omitted by the caller.
+ if [ -z "${VPC_CIDR}" ] && [ -f "${vsd}/cidr" ]; then
+ VPC_CIDR=$(cat "${vsd}/cidr" 2>/dev/null || true)
+ fi
+ if [ -z "${PUBLIC_VLAN}" ] && [ -f "${vsd}/ips/${PUBLIC_IP}.pvlan" ]; then
+ PUBLIC_VLAN=$(cat "${vsd}/ips/${PUBLIC_IP}.pvlan" 2>/dev/null || true)
+ fi
+
+ [ -z "${VPC_CIDR}" ] && die "update-vpc-source-nat-ip: missing --cidr
(or persisted vpc cidr)"
+ [ -z "${PUBLIC_VLAN}" ] && die "update-vpc-source-nat-ip: missing
--public-vlan"
+
+ log "update-vpc-source-nat-ip: vpc=${VPC_ID} ns=${NAMESPACE} old=?
new=${PUBLIC_IP} pvlan=${PUBLIC_VLAN} cidr=${VPC_CIDR}"
+
+ local old_source_nat_ip=""
+ local old_public_vlan=""
+ local f ip flag
+ for f in "${vsd}/ips/"*; do
+ [ -f "${f}" ] || continue
+ ip=$(basename "${f}")
+ case "${ip}" in
+ *.pvlan|*.tier) continue ;;
+ esac
+ flag=$(cat "${f}" 2>/dev/null || true)
+ if [ "${flag}" = "true" ]; then
+ old_source_nat_ip="${ip}"
+ break
+ fi
+ done
+
+ if [ -n "${old_source_nat_ip}" ] && [ -f
"${vsd}/ips/${old_source_nat_ip}.pvlan" ]; then
+ old_public_vlan=$(cat "${vsd}/ips/${old_source_nat_ip}.pvlan"
2>/dev/null || true)
+ fi
+
+ local new_pveth_h new_pveth_n pub_br
+ new_pveth_h=$(pub_veth_host_name "${PUBLIC_VLAN}" "${VPC_ID}")
+ new_pveth_n=$(pub_veth_ns_name "${PUBLIC_VLAN}" "${VPC_ID}")
+ ensure_host_bridge "${PUB_ETH}" "${PUBLIC_VLAN}"
+ pub_br=$(host_bridge_name "${PUB_ETH}" "${PUBLIC_VLAN}")
+
+ if ! ip link show "${new_pveth_h}" >/dev/null 2>&1; then
+ ip link add "${new_pveth_h}" type veth peer name "${new_pveth_n}"
+ ip link set "${new_pveth_n}" netns "${NAMESPACE}"
+ ip link set "${new_pveth_h}" master "${pub_br}"
+ ip link set "${new_pveth_h}" up
+ ip netns exec "${NAMESPACE}" ip link set "${new_pveth_n}" up
+ log "update-vpc-source-nat-ip: created public veth ${new_pveth_h} <->
${new_pveth_n}"
+ else
+ ip link set "${new_pveth_h}" up 2>/dev/null || true
+ ip netns exec "${NAMESPACE}" ip link set "${new_pveth_n}" up
2>/dev/null || true
+ fi
+
+ ensure_public_ip_on_namespace "${PUBLIC_IP}" "${PUBLIC_CIDR}"
"${new_pveth_n}" "${new_pveth_h}"
+ ip route replace "${PUBLIC_IP}/32" dev "${new_pveth_h}" 2>/dev/null || true
+
+ if [ -n "${old_source_nat_ip}" ] && [ "${old_source_nat_ip}" !=
"${PUBLIC_IP}" ] && [ -n "${old_public_vlan}" ]; then
+ local old_pveth_n
+ old_pveth_n=$(pub_veth_ns_name "${old_public_vlan}" "${VPC_ID}")
+ if [ "${old_pveth_n}" != "${new_pveth_n}" ]; then
+ ip netns exec "${NAMESPACE}" ip route show default 2>/dev/null | \
+ grep " dev ${old_pveth_n}\b" | \
+ while read -r route; do
+ ip netns exec "${NAMESPACE}" ip route del ${route}
2>/dev/null || true
+ done
+ fi
+ fi
+
+ if [ -n "${PUBLIC_GATEWAY}" ]; then
+ ip netns exec "${NAMESPACE}" ip route replace default \
+ via "${PUBLIC_GATEWAY}" dev "${new_pveth_n}" 2>/dev/null || \
+ ip netns exec "${NAMESPACE}" ip route add default \
+ via "${PUBLIC_GATEWAY}" dev "${new_pveth_n}" 2>/dev/null || true
+ log "update-vpc-source-nat-ip: default route via ${PUBLIC_GATEWAY} dev
${new_pveth_n}"
+ fi
+
+ local vpc_post_chain="${CHAIN_PREFIX}_${VPC_ID}_VPC_POST"
+ ensure_chain nat "${vpc_post_chain}"
+ ensure_jump nat POSTROUTING "${vpc_post_chain}"
+
+ # This chain is dedicated to VPC source NAT egress; rebuild to a single
rule.
+ ip netns exec "${NAMESPACE}" iptables -t nat -F "${vpc_post_chain}"
+ ip netns exec "${NAMESPACE}" iptables -t nat \
+ -A "${vpc_post_chain}" -s "${VPC_CIDR}" -o "${new_pveth_n}" -j SNAT
--to-source "${PUBLIC_IP}"
+
+ # Keep exactly one source-NAT marker: new public IP=true, all others=false.
+ for f in "${vsd}/ips/"*; do
+ [ -f "${f}" ] || continue
+ ip=$(basename "${f}")
+ case "${ip}" in
+ *.pvlan|*.tier) continue ;;
+ esac
+ echo "false" > "${f}"
+ done
+ echo "true" > "${vsd}/ips/${PUBLIC_IP}"
+ echo "${PUBLIC_VLAN}" > "${vsd}/ips/${PUBLIC_IP}.pvlan"
+
+ local _arping_bin
+ _arping_bin=$(_find_arping) || true
+ if [ -n "${_arping_bin}" ]; then
+ ip netns exec "${NAMESPACE}" "${_arping_bin}" -c 3 -U -I
"${new_pveth_n}" "${PUBLIC_IP}" \
+ >/dev/null 2>&1 || true
+ fi
+
+ _dump_iptables "${NAMESPACE}"
+ release_lock
+ log "update-vpc-source-nat-ip: done vpc=${VPC_ID}
old=${old_source_nat_ip:-none} new=${PUBLIC_IP}"
+}
+
##############################################################################
# Command: shutdown-vpc
# Removes the VPC namespace after all tiers have been shut down.
@@ -3590,6 +3720,7 @@ case "${COMMAND}" in
destroy-network) cmd_destroy_network "$@" ;;
# VPC lifecycle
implement-vpc) cmd_implement_vpc "$@" ;;
+ update-vpc-source-nat-ip) cmd_update_vpc_source_nat_ip "$@" ;;
shutdown-vpc) cmd_shutdown_vpc "$@" ;;
destroy-vpc) cmd_destroy_vpc "$@" ;;
assign-ip) cmd_assign_ip "$@" ;;
@@ -3624,7 +3755,7 @@ case "${COMMAND}" in
custom-action) cmd_custom_action "$@" ;;
"")
echo "Usage: $0 {implement-network|shutdown-network|destroy-network|" \
- "implement-vpc|shutdown-vpc|destroy-vpc|" \
+
"implement-vpc|update-vpc-source-nat-ip|shutdown-vpc|destroy-vpc|" \
"assign-ip|release-ip|" \
"add-static-nat|delete-static-nat|add-port-forward|delete-port-forward|" \
"config-dhcp-subnet|remove-dhcp-subnet|add-dhcp-entry|remove-dhcp-entry|set-dhcp-options|"
\