docs: retarget the proxmox plan at Proxmox VE 9.2

The host is 9.2.20, not 8.x. Three corrections that would have failed
at runtime:

- VM.Monitor was removed in PVE 9.0, so it is dropped from the role and
  the agent read rests on VM.GuestAgent.Audit alone.
- The GC-assist tag becomes proxy_operator: current tag validation takes
  only lowercase letters, digits and underscore, so proxy-operator would
  have been rejected on every Create. The description mark keeps its
  hyphenated keys; it is free text, not a tag.
- HTTP/2 is documented as absent from pveproxy on every release, so the
  client disabling it is a fact rather than a guess.

Two new Phase 0 facts: F22 for the 9.2 unified authentication-failure
message, which invalidates any 401 text remembered from 8.x, and F23 for
the node /tmp tmpfs move. F12 records that the upload endpoint takes
iso, vztmpl or import on 9.x and still refuses snippets, which is what
keeps the seed ISO route the only API-only path for user-data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-18 15:49:14 +00:00
co-authored by Claude Opus 5
parent 708989c40f
commit 2167819157
+51 -20
View File
@@ -7,10 +7,13 @@
The provider contract (`docs/provider-contract.md`), the spec template The provider contract (`docs/provider-contract.md`), the spec template
(`docs/provider-spec-template.md`) and the kvm2 build proved the workflow: (`docs/provider-spec-template.md`) and the kvm2 build proved the workflow:
spec first, then implementation against the contract, one commit per step. spec first, then implementation against the contract, one commit per step.
This plan adds a third VM-style backend, `proxmox`, on the Proxmox VE 8.x This plan adds a third VM-style backend, `proxmox`, on the Proxmox VE 9.x
REST API at `https://<host>:8006/api2/json`. The user has a real host at REST API at `https://<host>:8006/api2/json`. The user has a real host at
192.168.2.8 and prepares the API token and a cloud-init-capable template 192.168.2.8 running **9.2.20** (Debian 13 Trixie, QEMU 11) and prepares the
before Phase 0 (checklist below). API token and a cloud-init-capable template before Phase 0 (checklist
below). Everything below is written against 9.2; where 8.x differed the
difference is called out, because the appendix and the guide must name the
version they were proven on.
The implementation runs in sessions whose context gets compacted, so every The implementation runs in sessions whose context gets compacted, so every
phase is self-contained: it names what to read first, what it creates or phase is self-contained: it names what to read first, what it creates or
@@ -32,13 +35,14 @@ committed together with the code. Follow `CLAUDE.md` for the log format.
## Before Phase 0: what the user prepares on 192.168.2.8 ## Before Phase 0: what the user prepares on 192.168.2.8
Run as root on the Proxmox node. Drop any privilege `pveum` rejects on the Run as root on the Proxmox node. **`VM.Monitor` was removed in PVE 9.0**, so
installed version (`VM.GuestAgent.Audit` exists from 8.2; older releases use the agent read rests on `VM.GuestAgent.Audit` alone and the role below must
`VM.Monitor` for the agent read) and note it for F17. not mention `VM.Monitor`. Drop any privilege `pveum` still rejects and note
it for F17.
```bash ```bash
pveum user add proxy-operator@pve --comment "egress-proxies-operator" pveum user add proxy-operator@pve --comment "egress-proxies-operator"
pveum role add ProxyOperator -privs "VM.Allocate VM.Clone VM.Audit VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Disk VM.Config.Network VM.Config.Options VM.PowerMgmt VM.Monitor VM.GuestAgent.Audit Datastore.Audit Datastore.AllocateSpace Datastore.AllocateTemplate Sys.Audit SDN.Use" pveum role add ProxyOperator -privs "VM.Allocate VM.Clone VM.Audit VM.Config.CDROM VM.Config.CPU VM.Config.Memory VM.Config.Disk VM.Config.Network VM.Config.Options VM.PowerMgmt VM.GuestAgent.Audit Datastore.Audit Datastore.AllocateSpace Datastore.AllocateTemplate Sys.Audit SDN.Use"
pveum aclmod / -user proxy-operator@pve -role ProxyOperator pveum aclmod / -user proxy-operator@pve -role ProxyOperator
pveum user token add proxy-operator@pve operator --privsep 0 pveum user token add proxy-operator@pve operator --privsep 0
# The secret is printed once. Save it on the workstation as one line: # The secret is printed once. Save it on the workstation as one line:
@@ -71,7 +75,9 @@ else Phase 0 reads from the API.
## Design decisions made with the user (not re-opened) ## Design decisions made with the user (not re-opened)
- **Backend and client.** Proxmox VE 8.x REST API, JSON, HTTP/1.1 only. - **Backend and client.** Proxmox VE 9.x REST API, JSON, HTTP/1.1 only
(pveproxy offers no HTTP/2 on any release; only Proxmox Backup Server
does).
Thin client on `net/http` (`client-kind: thin-client`), no SDK, no new Thin client on `net/http` (`client-kind: thin-client`), no SDK, no new
dependency. Auth header dependency. Auth header
`Authorization: PVEAPIToken=<user>@<realm>!<tokenid>=<secret>`, read from a `Authorization: PVEAPIToken=<user>@<realm>!<tokenid>=<secret>`, read from a
@@ -110,9 +116,13 @@ else Phase 0 reads from the API.
mark window). Content, one per line: `proxy-operator-managed=true`, mark window). Content, one per line: `proxy-operator-managed=true`,
`proxy-operator-uid=<uid>`, `proxy-operator-created-at=<RFC3339>`. `proxy-operator-uid=<uid>`, `proxy-operator-created-at=<RFC3339>`.
`created-at: written-at-create`. The config step after the clone also sets `created-at: written-at-create`. The config step after the clone also sets
tag `proxy-operator` for UI filtering only. ListByTag: one tag `proxy_operator` for UI filtering only. **Underscore, not hyphen:**
current PVE tag validation accepts only lowercase letters, digits and
underscore, and a hyphen is rejected; the underscore form is valid under
both the strict and the permissive reading, so it is used regardless of
what F9 finds. ListByTag: one
`GET /cluster/resources?type=vm`; candidates are entries with tag `GET /cluster/resources?type=vm`; candidates are entries with tag
`proxy-operator` or name prefix `proxy-`; a `GET /nodes/{node}/qemu/{vmid}/config` `proxy_operator` or name prefix `proxy-`; a `GET /nodes/{node}/qemu/{vmid}/config`
per candidate for the description; unmarked skipped; one unreadable VM per candidate for the description; unmarked skipped; one unreadable VM
skipped with a V(1) line unless ctx ended; the resources call failing fails skipped with a V(1) line unless ctx ended; the resources call failing fails
the whole list (`list-partial: fail-whole`). the whole list (`list-partial: fail-whole`).
@@ -229,8 +239,11 @@ else Phase 0 reads from the API.
`status` values (`running`, `stopped`, `unknown`), `type` (`qemu`, `lxc`), `status` values (`running`, `stopped`, `unknown`), `type` (`qemu`, `lxc`),
`lock`, `tags` (separator), `template`; whether `lock` appears there during `lock`, `tags` (separator), `template`; whether `lock` appears there during
a clone. a clone.
- F9. Tag rules: allowed characters, case, length, `tags` separator on - F9. Tag rules on 9.2: confirm a hyphen is rejected and `proxy_operator`
`PUT config` and in `/cluster/resources`. accepted, plus case folding, length, and the `tags` separator on
`PUT config` and in `/cluster/resources`. The underscore form stays either
way; this fact only decides what the guide tells users about their own
tags.
- F10. `description` on `POST .../clone`: accepted, survives verbatim - F10. `description` on `POST .../clone`: accepted, survives verbatim
(newlines), max length; whether `name` accepts the 22-character (newlines), max length; whether `name` accepts the 22-character
`proxy-<base32>` form. `proxy-<base32>` form.
@@ -239,7 +252,9 @@ else Phase 0 reads from the API.
- F12. Upload endpoint: field names (`content`, `filename`, file part name - F12. Upload endpoint: field names (`content`, `filename`, file part name
`filename`), the filename rule (`.iso` suffix, no slashes), the response `filename`), the filename rule (`.iso` suffix, no slashes), the response
(UPID), the privilege it needs, and whether re-uploading an existing name (UPID), the privilege it needs, and whether re-uploading an existing name
is refused (then delete-then-upload) or overwrites. is refused (then delete-then-upload) or overwrites. On 9.x `content`
accepts `iso`, `vztmpl` or `import` and still refuses `snippets`, which is
why the seed travels as an ISO; confirm `iso` is what an `.iso` needs.
- F13. `DELETE /nodes/{node}/storage/{storage}/content/{volid}`: response - F13. `DELETE /nodes/{node}/storage/{storage}/content/{volid}`: response
shape (UPID or null) and the message for a missing volume. shape (UPID or null) and the message for a missing volume.
- F14. `status/current` on a stopped VM: is `qmpstatus` present; on a paused - F14. `status/current` on a stopped VM: is `qmpstatus` present; on a paused
@@ -251,16 +266,29 @@ else Phase 0 reads from the API.
(`OK` or the error string), and how long a full clone of the template (`OK` or the error string), and how long a full clone of the template
takes. takes.
- F17. Privileges: which of the prepared list the token really needs, in - F17. Privileges: which of the prepared list the token really needs, in
particular `SDN.Use` on `/sdn/zones/localnetwork/<bridge>` for the `net0` particular the exact ACL path that satisfies `SDN.Use` for the `net0`
write and `VM.GuestAgent.Audit` or `VM.Monitor` for the agent read. write (the 9.x docs describe it as "access SDN vnets and local network
- F18. HTTP/2 is not offered (`curl --http2 -I` falls back or fails), so the bridges" without spelling out the path for a plain bridge), and
client disables it. `VM.GuestAgent.Audit` for the agent read. Also whether the config step
needs `VM.Config.Cloudinit` even though only `ide2` is written.
- F18. HTTP/2 is not offered by pveproxy on any release (documented), so the
client disables it; confirm with `curl --http2 -I` that the connection
falls back to HTTP/1.1.
- F19. The template's disk key (`scsi0` or `virtio0`), its `boot` line, that - F19. The template's disk key (`scsi0` or `virtio0`), its `boot` line, that
ide2 is free and no `cloudinit` drive exists. ide2 is free and no `cloudinit` drive exists.
- F20. `resize` on a stopped VM: request shape, the message when the target - F20. `resize` on a stopped VM: request shape, the message when the target
is smaller or equal, and that it is synchronous. is smaller or equal, and that it is synchronous.
- F21. Agent response shape: `result[].name`, `result[]["ip-addresses"][]` - F21. Agent response shape: `result[].name`, `result[]["ip-addresses"][]`
with `ip-address`, `ip-address-type`. with `ip-address`, `ip-address-type`.
- F22. The 401 body and reason phrase on 9.2. PVE 9.2 unified the
authentication-failure message so a failed login no longer leaks whether
the user exists, so any 401 text remembered from 8.x is wrong. Capture it
fresh into `err_unauthorized.json`.
- F23. Where the upload endpoint stages the request body, given that PVE 9
moved the node's `/tmp` to a tmpfs capped at half of RAM and cleans `/tmp`
and `/var/tmp` periodically. A seed ISO is tens of KiB so this cannot
bite in practice, but the observation belongs in the guide next to the
size limit.
## Proxmox endpoint map for the seam ## Proxmox endpoint map for the seam
@@ -602,7 +630,7 @@ idioms (`fixture`, `assertProviderError`, `logCapture`).
calling `Clone` with `vmidFor(req.UID, rng, attempt)`, `isAlreadyExists` calling `Clone` with `vmidFor(req.UID, rng, attempt)`, `isAlreadyExists`
moving to the next attempt, exhaustion Transient; `waitTask` with moving to the next attempt, exhaustion Transient; `waitTask` with
`budgets.clone`; `uploadSeed`; `SetVMConfig` with `cores`, `memory`, `budgets.clone`; `uploadSeed`; `SetVMConfig` with `cores`, `memory`,
`agent=1`, `tags=proxy-operator`, `ide2=<volid>,media=cdrom`, and `net0` `agent=1`, `tags=proxy_operator`, `ide2=<volid>,media=cdrom`, and `net0`
rewritten from the cloned config with `bridge=<cfg.Bridge>` only when rewritten from the cloned config with `bridge=<cfg.Bridge>` only when
configured (MAC kept); `bootDisk(cfg)` and `Resize` when `diskGiB` is configured (MAC kept); `bootDisk(cfg)` and `Resize` when `diskGiB` is
above the current `size=`; `Start` and `waitTask` with `budgets.start`; above the current `size=`; `Start` and `waitTask` with `budgets.start`;
@@ -817,12 +845,15 @@ idioms (`fixture`, `assertProviderError`, `logCapture`).
- **Error text location and exact wording (F1 to F6)** decide the whole - **Error text location and exact wording (F1 to F6)** decide the whole
NotFound path; if messages vary by storage type or PVE minor version, the NotFound path; if messages vary by storage type or PVE minor version, the
matchers get one fixture per variant and the guide names the tested matchers get one fixture per variant and the guide names the tested
version. version. This is not hypothetical: 9.2 rewrote the authentication-failure
message (F22), so fixtures captured from an 8.x host would already be
wrong. Every matched string comes from 192.168.2.8 at 9.2.20, and the
guide says so.
- **Clone cleanup (F15).** A failed or interrupted clone may leave a VM with - **Clone cleanup (F15).** A failed or interrupted clone may leave a VM with
`lock=clone` that never clears; Create then waits out `budgets.clone` and `lock=clone` that never clears; Create then waits out `budgets.clone` and
returns Transient every time. The guide documents `qm unlock`; the returns Transient every time. The guide documents `qm unlock`; the
reconciler keeps retrying rather than latching. reconciler keeps retrying rather than latching.
- **`SDN.Use` (F17).** PVE 8 refuses `net0` writes to non-root users without - **`SDN.Use` (F17).** PVE 9 refuses `net0` writes to non-root users without
it; a missing privilege is a 403 Permanent that latches the Proxy until it; a missing privilege is a 403 Permanent that latches the Proxy until
its spec changes. The guide's privilege list is the mitigation. its spec changes. The guide's privilege list is the mitigation.
- **The ide2 seed and a template with a cloudinit drive** both claim - **The ide2 seed and a template with a cloudinit drive** both claim