proxmox: deployment files, sample, user guide and doc rows

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>
This commit is contained in:
2026-09-23 21:14:18 +00:00
co-authored by Claude Opus 5.5
parent 6e9ef26dcb
commit 7f2073f94b
14 changed files with 1407 additions and 67 deletions
+66 -1
View File
@@ -11,6 +11,7 @@
| gcp provider | `internal/provider/gcp` | library | — | Compute Engine VMs, four API calls, fire-and-forget ops |
| kvm provider | `internal/provider/kvm` | library | — | VMs on libvirt hosts over SSH-forwarded sockets; image cache, overlay disks, NoCloud seeds |
| kvm2 provider | `internal/provider/kvm2` | library | — | the same backend, built clean-room from the provider contract as a second type: per-RPC child spans, pool-space quota, per-call budgets, its own metadata namespace, credentials re-read on every dial |
| proxmox provider | `internal/provider/proxmox` | library | — | VMs on a Proxmox VE cluster through its REST API: full clones of a template, a NoCloud seed uploaded through the API, the VMID derived from the Proxy UID, every task waited on, HTTP child spans |
| Shared provider helpers | `internal/provider/nocloud`, `internal/provider/keyedlock` | library | — | the NoCloud seed ISO and vendor-data builder, and one-slot semaphores by key; used by kvm2 and proxmox |
| Health engine | `internal/health` | Runnable | yes | through-the-proxy probes, thresholds, transition events |
| Lease store | `internal/lease` | Runnable (expiry sweep) | no | in-memory leases + cooldowns, single mutex |
@@ -204,6 +205,37 @@ outlives its ctx or budget kills the pooled connection (the only way to unblock
go-libvirt), so other calls in flight on that host fail once with Transient and
retry; the next call redials. Each RPC is a child span kvm2.<Method> under
provider.*, plus kvm2.dial and kvm2.download.
proxmox provider (internal/provider/proxmox/) - the Proxmox VE REST API, one URL per cluster
prov.Create ──► placement checked against the catalogue (Permanent before any request)
GET /cluster/resources: a VM with the name → ownership check ─┐ every step
else: template config checked (not a template, cloud-init drive, │ idempotent,
no net0 or boot disk it needs = Permanent) │ under a
VMID = hash(UID) in vmidRange, probing on; a taken VMID → │ per-name lock
ownership check: wait out a lock, adopt our name + mark + │ shared with
UID, probe past another name, Permanent otherwise │ Delete; each
POST .../clone (full, description = the mark) → wait task │ task's UPID
upload seed ISO to isoStorage (overwrites) → wait task │ polled every
PUT .../config: cores, memory, agent=1, tag, ide2 = seed, │ 1s to its end
net0 bridge/tag when configured │
resize if diskGiB is larger → wait task │
start → wait task → poll status/current until running (60s) ─┘
prov.Get ──► /cluster/resources (absent = NotFound; lock or node unknown = Transient)
+ config (name and mark must match, else NotFound: VMID reused)
+ status/current (paused and hibernated = Stopped)
+ agent network-get-interfaces (first routable IPv4; agent not
up yet = Provisioning)
prov.Delete ──► resources + config: gone, not ours or unmarked = leave it;
hibernated = Permanent; stop → destroy (purge) → delete the seed
on the VM's node (or on every node when no VM of ours was left)
prov.ListByTag ─► one /cluster/resources for the whole cluster, one config read per
candidate (tag or proxy- name); the description's mark decides
Check ──► per node: /version, node status, isoStorage status, templates in
/cluster/resources
providerID = vms/<vmid>/<name>: no node, because VMs migrate; the node is looked
up in /cluster/resources on every call. Token read from its file on every
request. Each request is an HTTP <METHOD> child span from the parent-gated
transport wrapper; a cancelled request ends only itself.
```
The reconciler never watches provider-side resources (Pods or GCP VMs).
@@ -388,6 +420,12 @@ incoming trace context):
from a span decorator over the seam, applied once in the dialer, so no
RPC is unspanned. The `libvirt call` log line of an RPC is written
inside its span. The `kvm` provider has no child spans.
- proxmox child spans: one `HTTP <METHOD>` client span per API request,
from `tracing.RestConfigWrapper()` on the provider's own HTTP transport,
under `provider.*` or `providerhealth.check`. Parent-gated like the
Kubernetes API spans, so a request without a parent starts none. The
provider's logging decorator starts no span; its `proxmox api call` line
carries the operation span's IDs.
- Kubernetes API calls become client child spans via a wrapped
`rest.Config` transport — gated on an existing parent span, so informer
list/watch long-polls and leader-election renewals never create root
@@ -413,12 +451,13 @@ the slog handler's ctx.
Answers "can this cluster reach each provider's backend right now?"
without waiting for a Proxy to fail. A provider opts in by implementing
`provider.Checker`: `Check(ctx) []TargetStatus` returns one verdict per
target and never fails as a whole. All four providers do:
target and never fails as a whole. All five providers do:
| Provider | Target | What one check does |
|---|---|---|
| kvm | each `kvm.hosts` entry | over the pooled SSH+libvirt connection: libvirt version and hostname, then the configured pool must exist and be running; reports pool capacity and free space ([details](kvm-provider.md#health-check)) |
| kvm2 | each `kvm2.hosts` entry | on a fresh SSH+libvirt connection dialled for the check and closed after it: libvirt version and hostname, then the configured pool must exist and be running; `details` carry `user`, `pool`, `hostname`, `libvirtVersion`, `poolState`, `poolCapacityGiB` and `poolAvailableGiB` ([details](kvm2-provider.md#health-check)) |
| proxmox | each `proxmox.nodes` entry | four requests on the shared HTTP client: API version, node status, the `isoStorage` status (it must be active and hold `iso` content), and the cluster's resources (every configured template must exist and be a template); `details` carry `isoStorage`, `tokenID`, `pveVersion`, `kernel`, `isoStorageAvailGiB` and `templates` ([details](proxmox-provider.md#health-check)) |
| kubernetes | `apiserver` | lists at most one managed Pod cluster-wide: the verb and scope `ListByTag` and GC rely on |
| gcp | the project | one page of an aggregated instance list with `maxResults=1`: credentials, Compute API enabled, `compute.instances.list` |
@@ -704,3 +743,29 @@ they read as choices, not accidents. Chronological by build step.
the Secret needs no restart; nothing is cached. The constructor only
checks that the files exist, to warn early without failing the provider
set.
- **proxmox derives the VMID from the Proxy UID and checks ownership
before using a taken one.** Proxmox addresses VMs by VMID, and names are
not unique, so the name cannot be the key. A hash of the UID placed in
`vmidRange` makes a retry land on the same VMID; a taken one is waited
on while locked, then adopted when it has our name, mark and UID,
probed past when it has another name, and refused otherwise. The wait
is what stops a retry after a crash mid-clone from cloning a duplicate:
a clone in progress has no name yet, so the by-name lookup misses it.
- **proxmox writes its mark into the VM's description**, in the clone
call itself, so no VM of ours exists unmarked. Tags cannot be passed to
the clone, and the clone's own creation time is copied from the
template. The `proxy_operator` tag is set afterwards for the Proxmox UI
and to narrow GC's candidates; the mark decides.
- **proxmox delivers cloud-init as a seed ISO uploaded through the API**,
not as Proxmox's own cloud-init drive or a `cicustom` snippet. The
upload endpoint does not accept snippets, and the operator has no shell
on the nodes; the seed code is kvm2's, extracted into
`internal/provider/nocloud`. Templates must therefore carry no Proxmox
cloud-init drive, and seeds of VMs destroyed outside the operator are a
documented GC blind spot.
- **proxmox resolves a VM's node on every call** from
`/cluster/resources`, and its providerID names none, because VMs migrate
between nodes. The cost is one extra request per operation; the gain is
that a migrated VM stays addressable and the listing, which includes the
guests of an unreachable node as `status: unknown`, never mistakes a
down node for a deleted VM.