Add network editing with VLAN, firewall, IPv4/IPv6 and undo

Press n in the info screen to edit one interface: bridge, VLAN tag,
firewall, IPv4 (DHCP, static or none) and IPv6 (SLAAC, DHCP, static or
none). The netN string is parsed, the chosen fields are changed and
the string is written back, so hwaddr, model=MAC, mtu, rate, trunks
and type are kept. For VMs, IP settings go to cloud-init ipconfigN and
only when the VM has a cloud-init drive.

A confirm shows the old and new raw values before anything is sent.
Press u to restore the values from before the last change to that
guest in this session.

Review fixes:
- Undo entries are cleared on a context switch, because VMIDs repeat
  across clusters.
- has_cloudinit only looks at disks.
- CIDR needs a decimal prefix: no netmask form and no /0.
- Fields you did not touch are not rewritten.

Live test on CT 104: firewall, VLAN 99, IPv6 DHCP and static IPv4 were
each applied and undone, and net0 ended identical to the baseline.
Going from static back to DHCP left the CT without IPv4 until a
reboot, so the confirm now warns that IP changes on a running
container may need one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit was merged in pull request #4.
This commit is contained in:
DuckbootandClaude Opus 5.5 committed 2026-11-02 21:45:06 +01:00
1 parent b874996b1d
commit 5038148686
9 files changed
+1422 -11

No files matched your search

+100
View File
@@ -346,3 +346,103 @@ Observed CLI behaviour (cv4pve-cli 2.4.0, all read-only probes):
- [x] Read-only live check: open InfoScreen for CT 104 and VM 108, data shown.
- [x] User tests "Backup now" on CT 104 (`debian (manual)`, 2026-11-02 21:09).
- [ ] Push `feat/guest-info`, merge to `main` (user-approved after the test).
---
# Network editing - delivery B
## Problem
Change a guest's network settings from the info screen: bridge, VLAN, firewall, IPv4
and IPv6 (incl. SLAAC), without breaking what is not edited.
## Decisions (user-approved)
- VM IP settings go through cloud-init `ipconfigN`, only when the VM has a cloud-init
drive; they apply at the next boot. VMs without cloud-init: IP fields disabled with a
note (IP is configured inside the VM).
- IPv4 modes: DHCP / Static (CIDR + optional gateway) / None.
IPv6 modes: SLAAC / DHCP / Static (CIDR + optional gateway) / None.
- Undo key `u`: restores the values from before the last network change made in this
app session for that guest.
- Out of scope: adding/removing interfaces, MAC, mtu, rate, trunks, DNS.
API facts (Proxmox VE 9.2, `api usage ... config set -v`):
- LXC `net[n]`: `name=<string>[,bridge][,firewall=<1|0>][,gw=<IPv4>][,gw6=<IPv6>]
[,host-managed][,hwaddr][,ip=<IPv4/CIDR|dhcp|manual>][,ip6=<IPv6/CIDR|auto|dhcp|manual>]
[,link_down][,mtu][,rate][,tag=<int>][,trunks][,type=veth]`. `ip6=auto` = SLAAC.
Applied immediately on a running CT.
- VM `net[n]`: `[model=]<enum>[,bridge][,firewall=<1|0>][,link_down][,macaddr][,mtu]
[,queues][,rate][,tag=<int>][,trunks][,<model>=<macaddr>]`. Applied immediately.
- VM `ipconfig[n]`: `[gw=<IPv4>][,gw6=<IPv6>][,ip=<IPv4/CIDR|dhcp>][,ip6=<IPv6/CIDR|dhcp|auto>]`
(cloud-init; "no explicit gateway with dhcp"). Applies at the next boot.
- `delete=<list>` removes settings (used when `ipconfigN` ends up empty).
- `get node network <node> -o json` -> list of dicts with `iface`, `type`
(`bridge`, `OVSBridge`, `eth`, ...), `active`.
- Cloud-init drive: a disk value containing `cloudinit`
(`ide2 = local-lvm:vm-108-cloudinit,media=cdrom`).
### `cli.py`
- `bridges(guest) -> list[str]` - `iface` of entries with type `bridge` or `OVSBridge`
on the guest's node, sorted.
- `format_net(d: dict) -> str` - inverse of `parse_net`: `k=v` joined by `,`, order
kept. `format_net(parse_net(s)) == s` for the real samples.
- `has_cloudinit(config) -> bool`.
- `apply_link(net, bridge, tag, firewall) -> dict` - a copy of `net`: `bridge` set;
`tag` int 1-4094 or None (None removes `tag`); `firewall` True -> `"1"`, False ->
`firewall` removed. Existing keys keep their position; new keys are appended.
- `ip_mode(cfg, family) -> tuple[str, str, str]` - `(mode, cidr, gw)` read from a
parsed LXC net or VM ipconfig dict; family 4 uses `ip`/`gw`, 6 uses `ip6`/`gw6`.
Modes: `dhcp`, `static`, `none`, and for 6 also `auto`; `manual` reads as `none`.
- `apply_ip(cfg, family, mode, cidr="", gw="") -> dict` - copy with the family's keys
set: `dhcp`/`auto` -> `ip[6]=dhcp|auto`, gateway removed; `static` -> `ip[6]=<cidr>`,
`gw[6]` set if given else removed; `none` -> both removed. Validation (ValueError):
static needs a CIDR of the right family with a `/prefix`
(`ipaddress.ip_interface`); a gateway must be an address of the same family; `auto`
only for family 6.
- `set_net(guest, changes: dict[str, str | None])` -> one call:
`set <kind> config <node> <vmid> [--<key> <value> ...] [--delete <k1,k2>] --wait`;
`None` values go to `--delete`. Empty `changes` -> ValueError.
### `app.py` (UI contract)
- Key `n` in InfoScreen opens `NetScreen` for the guest (needs the loaded config;
nothing if the guest has no `netN`).
- `NetScreen(ModalScreen)`: `Select#iface` (netN keys), `Select#bridge`
(`cli.bridges`), `Input#tag` (empty = none), `Checkbox#firewall`, `Select#ipv4`
(`dhcp`/`static`/`none`), `Input#ipv4_cidr`, `Input#ipv4_gw`, `Select#ipv6`
(`auto` labelled SLAAC / `dhcp` / `static` / `none`), `Input#ipv6_cidr`,
`Input#ipv6_gw`, buttons `#apply` / `#cancel`; escape = cancel. Prefilled from the
current values of the selected interface; changing `#iface` prefills again.
VM without cloud-init: IP widgets disabled and `Static#ip_note` explains why.
- Apply: build the new netN (and for VMs the new ipconfigN) with `apply_link` /
`apply_ip`; ValueError -> error notify, screen stays open, no CLI call. No change ->
info notify, nothing else. Otherwise `ConfirmScreen` listing each changed key with
its old and new value (raw strings); for a running guest it says link changes apply
immediately, and for VM ipconfig that it applies at next boot.
- On yes: guest busy (refuse if busy), worker runs `cli.set_net(guest, changes)`,
done/error notify, busy released, InfoScreen reloads. On success the old values
(`None` for keys that did not exist) are stored as the guest's undo entry.
- Live finding (CT 104): Proxmox applies a changed `ip` on a running CT, but static ->
dhcp left the CT without IPv4 (no DHCP client restart) until the CT was rebooted.
The confirm therefore says, for IP changes on a running container, that a reboot of
the container may be needed. Proxmox re-orders keys when it stores `netN`.
- Review fixes: undo entries are cleared on a context switch (VMIDs repeat across
clusters); `has_cloudinit` only looks at disk keys; CIDR needs a decimal prefix
1-32 / 1-128 (no netmask form, no /0); fields the user did not touch are not rewritten
(e.g. `ip6=manual` stays).
- Key `u` in InfoScreen: no undo entry -> info notify. Otherwise `ConfirmScreen`
showing what will be restored, then `cli.set_net(guest, old_values)` the same way;
the undo entry is cleared after success.
## Steps
- [x] Tests - red first.
- [x] Implementation - suite green, ruff clean, README keys `n` / `u`.
- [x] Live on CT 104 (approved), each step reverted: firewall off->on->off; VLAN
none->99->none (undo key); IPv6 SLAAC->DHCP->SLAAC; IPv4 DHCP->static
172.16.41.200/23 gw 172.16.40.1->DHCP. Bridge change cannot be tested live: the
node has a single bridge (`vmbr0`). Final net0 string identical to the baseline.
- [ ] Push `feat/network-edit`, merge to `main` (ask first).