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:
1 parent
b874996b1d
commit
5038148686
9 files changed
+1422
-11
No files matched your search
@@ -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).
|
||||
Reference in new issue
Block a user