Adds proxmox, a third VM-style provider: Proxy VMs are full clones of a template on a Proxmox VE 9.x cluster, driven through the REST API with an API token. No SSH, no new Go dependency.
How it works
Create blocks until the VM runs: clone the template (VMID derived from the Proxy UID, ownership mark written in the clone call), upload a NoCloud seed ISO, write the config (cores, memory, agent, ide2, optional bridge/VLAN tag keeping the MAC), grow the disk, start. Every step is a Proxmox task that is waited on, and every step resumes after a crash.
Get: the address comes from the QEMU guest agent, which a vendor-data script installs at first boot.
Delete: stop, delete the seed, destroy with purge=1&destroy-unreferenced-disks=1.
ListByTag covers the whole cluster in one listing.
A health Checker runs per node: version, node, ISO storage, templates.
Where to read
Guide: docs/proxmox-provider.md
Design, with evidence per decision: docs/proxmox-provider-spec.md
How it meets the provider contract: docs/providers-existing.md
Plan and per-phase log: docs/plans/2026-09-18-1502-proxmox-provider.md, docs/plans-executions/2026-09-18-1502-proxmox-provider.md
Shared code: the NoCloud seed builder and the per-name lock moved from kvm2 into internal/provider/nocloud and internal/provider/keyedlock. It is a pure move; kvm2's behaviour is unchanged.
Review: two passes (go-operator-reviewer, /code-review high), 19 findings. 9 fixed with regression tests, 3 accepted as documented limits, 7 rejected with reasons. See docs/reviews/2026-09-23-proxmox-provider-review/REPORT.md. The main fix: Delete now removes the seed ISO before destroying the VM, so a failed seed delete can no longer leak the ISO.
Validation
make test is green; proxmox package coverage is 97%.
make test-integration-proxmox passed on a real PVE 9.2.20 node: Create 6.2 s, guest address 50 s later, Delete leaves no VM and no seed.
Three API answers the design had only guessed were captured on the host. Two corrected the code:
a missing storage is a 400;
a hibernated VM can be destroyed.
A kind pass took the sample Proxy to Ready, proxied a request through it, and deleted it cleanly.
Known limits are in the guide's Known gaps. Cross-node paths are untested, because the test host has one node.
Also in this MR: the two plan commits (708989c, 2167819) that were committed to local main but never pushed.
make lint panics inside staticcheck on the local Go 1.27 toolchain. Lint without staticcheck is at its baseline.
Adds `proxmox`, a third VM-style provider: Proxy VMs are full clones of a template on a Proxmox VE 9.x cluster, driven through the REST API with an API token. No SSH, no new Go dependency.
**How it works**
- **Create** blocks until the VM runs: clone the template (VMID derived from the Proxy UID, ownership mark written in the clone call), upload a NoCloud seed ISO, write the config (cores, memory, agent, `ide2`, optional bridge/VLAN tag keeping the MAC), grow the disk, start. Every step is a Proxmox task that is waited on, and every step resumes after a crash.
- **Get**: the address comes from the QEMU guest agent, which a vendor-data script installs at first boot.
- **Delete**: stop, delete the seed, destroy with `purge=1&destroy-unreferenced-disks=1`.
- **ListByTag** covers the whole cluster in one listing.
- A health **Checker** runs per node: version, node, ISO storage, templates.
**Where to read**
- Guide: `docs/proxmox-provider.md`
- Design, with evidence per decision: `docs/proxmox-provider-spec.md`
- How it meets the provider contract: `docs/providers-existing.md`
- Plan and per-phase log: `docs/plans/2026-09-18-1502-proxmox-provider.md`, `docs/plans-executions/2026-09-18-1502-proxmox-provider.md`
**Shared code:** the NoCloud seed builder and the per-name lock moved from kvm2 into `internal/provider/nocloud` and `internal/provider/keyedlock`. It is a pure move; kvm2's behaviour is unchanged.
**Review:** two passes (`go-operator-reviewer`, `/code-review high`), 19 findings. 9 fixed with regression tests, 3 accepted as documented limits, 7 rejected with reasons. See `docs/reviews/2026-09-23-proxmox-provider-review/REPORT.md`. The main fix: Delete now removes the seed ISO before destroying the VM, so a failed seed delete can no longer leak the ISO.
**Validation**
- `make test` is green; proxmox package coverage is 97%.
- `make test-integration-proxmox` passed on a real PVE 9.2.20 node: Create 6.2 s, guest address 50 s later, Delete leaves no VM and no seed.
- Three API answers the design had only guessed were captured on the host. Two corrected the code:
- a missing storage is a 400;
- a hibernated VM can be destroyed.
- A kind pass took the sample Proxy to Ready, proxied a request through it, and deleted it cleanly.
**Known limits** are in the guide's Known gaps. Cross-node paths are untested, because the test host has one node.
**Also in this MR:** the two plan commits (`708989c`, `2167819`) that were committed to local `main` but never pushed.
`make lint` panics inside staticcheck on the local Go 1.27 toolchain. Lint without staticcheck is at its baseline.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Eleven self-contained phases (host recon and fixtures, spec, config and
skeleton, nocloud/keyedlock extraction from kvm2, HTTP client and fake,
Get/List, Create, Delete/Checker, test sweep and integration test,
deployment and docs, review and MR), with the Proxmox host setup
checklist and the backend facts Phase 0 must verify on 192.168.2.8.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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>
Phase 0 of the proxmox provider plan. 55 fixtures under
internal/provider/proxmox/testdata, each the status, reason phrase and
raw body of a real response from the user's host, and the execution log
answering the plan's facts F1 to F23.
Recon ran a clone of the Debian 13 template through the whole path the
provider will take, twice: untagged and on VLAN 4. On both, the NoCloud
seed on ide2 installed the guest agent and an address came back about 50
seconds after start. It also found two bugs in the plan: a VM being
cloned has no name in /cluster/resources, so name-first lookups would read
it as gone, and a crash-retry would clone a duplicate. Those and eight
smaller corrections are listed for the spec.
Decisions made with the user: seeds go to a dedicated proxy-seeds
storage where the token can delete them; proxies land on a VLAN set in
config (vlanTag); the API URL is https://192.168.0.8:8006, the address in
the node certificate.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phase 1 of the proxmox provider plan: docs/proxmox-provider-spec.md
answers all 24 entries of the provider spec template against the
contract, citing the Phase 0 facts from pv1-1 as evidence.
It folds in the ten corrections recon found. The two that matter most:
Create inspects a taken VMID, waiting out a clone lock, before probing
past it, so a crash-retry adopts its own half-cloned VM instead of cloning
a duplicate; and Get checks the lock before the name, because a VM being
cloned has no name yet.
The plan now opens with a note that the spec wins where they disagree.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phase 2 of the proxmox provider plan. Adds the proxmox: config block with
its validation, registers the type in the config allow-list and the
constructor map, and starts internal/provider/proxmox: the api seam and
its data types, providerIDs (vms/<vmid>/<name>) with the UID-derived VMID,
the description mark, state mapping, and error classification.
Tests read the responses captured from pv1-1 in Phase 0 rather than
repeating Proxmox's wording, and every matcher is run against every
captured error so none fires on the wrong message. The contract methods
are Transient stubs until Phases 5 to 7.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
kvm2's NoCloud seed builder moves to internal/provider/nocloud (exported,
with an ErrEmptyName sentinel instead of permanentf) and its keyed locks
to internal/provider/keyedlock, tests moved under their existing names.
kvm2 behaviour is unchanged. proxmox gains the per-name lock and the pure
seed helpers (seedISOName, seedVolID, buildSeed); unlike kvm2 (review
finding S5.1), a seed staging failure there is Transient, not Permanent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
httpAPI implements the sixteen seam methods over net/http: the token is
read per request, non-2xx answers become apiError from the body message or
the reason phrase, echoed secrets are redacted, the CA loads lazily, HTTP/2
is off, and uploads stream with an exact Content-Length. The observed
decorator logs one V(1) line per call and starts no span; New wires both.
fakeAPI simulates clone locks, tasks and ISOs with the captured error texts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Get follows spec entry 9's check order (lock and unknown status before
the name, suspended is Stopped after the name and mark checks), reads
status/current so paused is Stopped, and maps the three agent messages
of entry 7. ListByTag reads one config per candidate and fails whole
only on the resources call or an ended ctx. waitTask polls the UPID's
node every budgets.poll, logs failed tasks at V(1) and counts
"WARNINGS: <n>" as success; waitUnlocked serves Create's ownership check.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Create validates placement, then under the per-name lock adopts the VM a
previous attempt left (by name, or at a taken VMID after the ownership
check of spec entry 2) or full-clones the template onto the UID's
deterministic VMID with the mark in the clone call. It then uploads the
seed over any existing file, writes cores, memory, agent, tag, ide2 and
net0 when configured, grows the boot disk when diskGiB is larger, starts,
and waits for running/running.
waitUnlocked no longer waits on a suspended lock: a hibernated VM is
judged at once, so ours is Permanent and a foreign one is probed past.
The template is checked only on the allocation path, and its shape
(net0, boot disk size) is refused before any clone exists. Spec entries
2, 4 and 10 updated to match.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Delete stops and destroys a VM of ours under the per-name lock, then
deletes its seed on the VM's node, or on every node when no VM of ours
was left. A hibernated VM of ours is Permanent, as in Create. The
Checker verifies each node concurrently (version, node status, ISO
storage, templates) and adds CertificateInvalid, CredentialsUnavailable,
StorageNotFound and TemplateNotFound to the shared reasons. The
ProviderHealth type comment gains proxmox.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The contract sweep in provider_test.go: every method and Check returns
within a second of a cancel, every wrapErr path carries the configured
name, op and id, the V0/V1/V2 logging tiers and the no-op lines, and a
Get over the real client whose HTTP spans are children of provider.get.
proxmox_integration_test.go (-tags=integration) holds TestProxmox_lifecycle
and TestProxmoxCheck_realHost; both compile and skip without
PROXMOX_TEST_*, and have not run against a host yet. make
test-integration-proxmox runs them. docs/testing.md gains the proxmox,
nocloud and keyedlock rows, the Live Proxmox gaps and the integration
section; spec entry 21's section 11 list is ticked.
Waits that could hang the package under a mutation are bounded: the
concurrent-Create loop and eight direct blocking calls in older tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The optional proxmox-token Secret (token, ca.pem) mounted at
/etc/proxy-operator/proxmox, make proxmox-token-secret with content
checks that never print the token, a commented proxmox-home entry in
both providers configs, and config/samples/proxy_proxmox.yaml.
TestLoadConfig_proxmoxSampleEntry loads the commented entry from both
files, and a second test ties the sample Proxy's placement to it.
docs/proxmox-provider.md is the user guide: prerequisites and privileges,
cluster setup, config reference, placement, what lands on Proxmox,
logging, tracing, timeouts, health check, error classes, known gaps and
testing. providers-existing.md gains the proxmox column and section;
architecture.md, the README, providers.md and CLAUDE.md gain their rows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Phase 10. Two review passes (go-operator-reviewer, /code-review high)
over the branch, recorded in docs/reviews/2026-09-23-proxmox-provider-review/:
9 findings fixed, 3 accepted as documented limits, 7 rejected with a
reason.
- Delete removes the seed between the stop and the destroy, so a failed
seed delete leaves the VM for the next Delete instead of leaking the
ISO; a Permanent refusal on the every-node path fails the call.
- Create skips a probe VMID whose listed name is someone else's without
waiting on its lock.
- The three answers Phase 0 never captured are fixtures now: a missing
storage is a 400 (the Checker reports StorageNotFound), a hibernated
VM is destroyable (Delete destroys ours), a paused VM stops.
- TestProxmox_lifecycle and TestProxmoxCheck_realHost passed on pv1-1
(PVE 9.2.20); the docs record the run and drop "not yet run".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
kacerr
merged commit 78477d6e8f into main2026-09-24 13:12:45 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds
proxmox, a third VM-style provider: Proxy VMs are full clones of a template on a Proxmox VE 9.x cluster, driven through the REST API with an API token. No SSH, no new Go dependency.How it works
ide2, optional bridge/VLAN tag keeping the MAC), grow the disk, start. Every step is a Proxmox task that is waited on, and every step resumes after a crash.purge=1&destroy-unreferenced-disks=1.Where to read
docs/proxmox-provider.mddocs/proxmox-provider-spec.mddocs/providers-existing.mddocs/plans/2026-09-18-1502-proxmox-provider.md,docs/plans-executions/2026-09-18-1502-proxmox-provider.mdShared code: the NoCloud seed builder and the per-name lock moved from kvm2 into
internal/provider/nocloudandinternal/provider/keyedlock. It is a pure move; kvm2's behaviour is unchanged.Review: two passes (
go-operator-reviewer,/code-review high), 19 findings. 9 fixed with regression tests, 3 accepted as documented limits, 7 rejected with reasons. Seedocs/reviews/2026-09-23-proxmox-provider-review/REPORT.md. The main fix: Delete now removes the seed ISO before destroying the VM, so a failed seed delete can no longer leak the ISO.Validation
make testis green; proxmox package coverage is 97%.make test-integration-proxmoxpassed on a real PVE 9.2.20 node: Create 6.2 s, guest address 50 s later, Delete leaves no VM and no seed.Known limits are in the guide's Known gaps. Cross-node paths are untested, because the test host has one node.
Also in this MR: the two plan commits (
708989c,2167819) that were committed to localmainbut never pushed.make lintpanics inside staticcheck on the local Go 1.27 toolchain. Lint without staticcheck is at its baseline.🤖 Generated with Claude Code