proxmox: config block, IDs, marks, state mapping, classify, registration

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>
This commit is contained in:
2026-09-23 09:42:46 +00:00
co-authored by Claude Opus 5.5
parent 4e974f30f6
commit 6e996ae2c7
19 changed files with 1856 additions and 4 deletions
+52
View File
@@ -0,0 +1,52 @@
package proxmox
import (
"net/netip"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
// mapState turns status/current's status and qmpstatus plus the discovered
// address into the contract's state (spec entry 9). A VM is Running only
// with an address; prelaunch is still starting; any other qmpstatus of a
// running VM (paused, suspended, io-error, …) and any status not recognised
// is Stopped, which the reconciler answers by replacing the VM. An empty
// qmpstatus is taken as running: the field is missing, not a state.
func mapState(status, qmpStatus, ip string) provider.InstanceState {
if status != "running" {
return provider.StateStopped
}
switch qmpStatus {
case "running", "":
if ip == "" {
return provider.StateProvisioning
}
return provider.StateRunning
case "prelaunch":
return provider.StateProvisioning
default:
return provider.StateStopped
}
}
// firstIPv4 picks the first usable IPv4 from the guest agent's interfaces,
// in the agent's order: not on the loopback interface, not loopback,
// link-local or unspecified. Empty means no address yet.
func firstIPv4(ifaces []agentInterface) string {
for _, iface := range ifaces {
if iface.Name == "lo" {
continue
}
for _, a := range iface.IPAddresses {
if a.Type != "ipv4" {
continue
}
addr, err := netip.ParseAddr(a.Address)
if err != nil || !addr.Is4() || addr.IsLoopback() || addr.IsLinkLocalUnicast() || addr.IsUnspecified() {
continue
}
return addr.String()
}
}
return ""
}