Files
egress-proxies-operator/internal/provider/proxmox/state.go
T
kacerrandClaude Opus 5.5 3d72507894 proxmox: review fixes, real-host validation record, changelog
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>
2026-09-24 10:28:28 +00:00

56 lines
1.8 KiB
Go

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 {
switch {
case guestRunning(status, qmpStatus):
if ip == "" {
return provider.StateProvisioning
}
return provider.StateRunning
case status == "running" && qmpStatus == "prelaunch":
return provider.StateProvisioning
default:
return provider.StateStopped
}
}
// guestRunning reports a VM whose guest runs, the only state in which its
// agent can answer and mapState can call it Running.
func guestRunning(status, qmpStatus string) bool {
return status == "running" && (qmpStatus == "running" || qmpStatus == "")
}
// 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 ""
}