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:
+66
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user