proxmox: Get, ListByTag and task polling
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>
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
package proxmox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
|
||||
)
|
||||
|
||||
// lockSuspended is the lock of a VM hibernated to disk: it is not a task in
|
||||
// progress, and the VM stays that way until someone resumes it.
|
||||
const lockSuspended = "suspended"
|
||||
|
||||
// Get is spec entries 7 to 9: the VMID's resource entry, then its config
|
||||
// for the name and the mark, then status/current and, for a running VM, the
|
||||
// guest agent. A running Get makes four calls.
|
||||
func (p *Provider) Get(ctx context.Context, id string) (*provider.Instance, error) {
|
||||
vmid, name, err := parseProviderID(id)
|
||||
if err != nil {
|
||||
return nil, p.wrapErr("get", id, err)
|
||||
}
|
||||
log := p.logger(ctx).WithValues("vmid", vmid, "name", name)
|
||||
inst, err := p.get(ctx, log, vmid, name)
|
||||
if err != nil {
|
||||
err = p.wrapErr("get", id, err)
|
||||
if errors.Is(err, provider.ErrNotFound) {
|
||||
log.V(1).Info("proxmox vm not found", "reason", err.Error())
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
func (p *Provider) get(ctx context.Context, log logr.Logger, vmid int, name string) (*provider.Instance, error) {
|
||||
r, ok, err := p.resolve(ctx, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: VM %d is not in the cluster's resources", provider.ErrNotFound, vmid)
|
||||
}
|
||||
log = log.WithValues("node", r.Node)
|
||||
// A mid-clone entry has no name and status unknown, so the lock is
|
||||
// checked before anything that would read it as absent or foreign.
|
||||
if r.Lock != "" && r.Lock != lockSuspended {
|
||||
return nil, transientf("VM %d is locked (%s)", vmid, r.Lock)
|
||||
}
|
||||
if r.Status == "unknown" {
|
||||
return nil, transientf("VM %d has status unknown: its node %s is not answering", vmid, r.Node)
|
||||
}
|
||||
cfg, err := p.api.VMConfig(ctx, r.Node, vmid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading config of VM %d: %w", vmid, err)
|
||||
}
|
||||
if lock := cfg["lock"]; lock != "" && lock != lockSuspended {
|
||||
return nil, transientf("VM %d is locked (%s)", vmid, lock)
|
||||
}
|
||||
if cfg["name"] != name {
|
||||
return nil, fmt.Errorf("%w: VM %d is now named %q: the VMID was reused", provider.ErrNotFound, vmid, cfg["name"])
|
||||
}
|
||||
mark := parseDescription(cfg["description"])
|
||||
if !mark.Managed {
|
||||
return nil, fmt.Errorf("%w: VM %d carries no proxy-operator mark", provider.ErrNotFound, vmid)
|
||||
}
|
||||
inst := &provider.Instance{ID: formatProviderID(vmid, name), UID: mark.UID, CreatedAt: mark.CreatedAt}
|
||||
|
||||
// A hibernated VM is Stopped whatever status/current says; it needs no
|
||||
// further call.
|
||||
if r.Lock == lockSuspended || cfg["lock"] == lockSuspended {
|
||||
inst.State = provider.StateStopped
|
||||
log.V(1).Info("proxmox vm fetched", "state", inst.State, "lock", lockSuspended, "uid", inst.UID)
|
||||
return inst, nil
|
||||
}
|
||||
st, err := p.api.VMStatus(ctx, r.Node, vmid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading status of VM %d: %w", vmid, err)
|
||||
}
|
||||
if st.Status == "running" && (st.QMPStatus == "running" || st.QMPStatus == "") {
|
||||
ip, state, err := p.agentAddress(ctx, log, r.Node, vmid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inst.IP = ip
|
||||
inst.State = state
|
||||
} else {
|
||||
inst.State = mapState(st.Status, st.QMPStatus, "")
|
||||
}
|
||||
log.V(1).Info("proxmox vm fetched", "state", inst.State, "ip", inst.IP, "uid", inst.UID, "status", st.Status, "qmpstatus", st.QMPStatus)
|
||||
return inst, nil
|
||||
}
|
||||
|
||||
// agentAddress asks the guest agent of a running VM for its address and
|
||||
// turns its answers into a state (spec entry 7). Any answer it cannot place
|
||||
// is Transient, whatever its status: an address read never latches or
|
||||
// loses a Proxy.
|
||||
func (p *Provider) agentAddress(ctx context.Context, log logr.Logger, node string, vmid int) (string, provider.InstanceState, error) {
|
||||
ifaces, err := p.api.AgentNetworkInterfaces(ctx, node, vmid)
|
||||
switch {
|
||||
case err == nil:
|
||||
ip := firstIPv4(ifaces)
|
||||
log.V(2).Info("proxmox guest agent answered", "interfaces", len(ifaces), "ip", ip)
|
||||
return ip, mapState("running", "running", ip), nil
|
||||
case isAgentNotRunning(err):
|
||||
return "", provider.StateProvisioning, nil
|
||||
case isAgentNotConfigured(err):
|
||||
// agent=1 was removed: this VM can never report an address, so it
|
||||
// is replaced by one that can.
|
||||
return "", provider.StateStopped, nil
|
||||
default:
|
||||
// Includes "VM <vmid> is not running": it stopped since the status
|
||||
// read, and the next Get sees that directly.
|
||||
return "", "", fmt.Errorf("%w: reading the guest agent of VM %d: %w", provider.ErrTransient, vmid, err)
|
||||
}
|
||||
}
|
||||
|
||||
// resolve finds a VMID in the cluster's resources. Only QEMU VMs count: a
|
||||
// container at the VMID means ours is gone.
|
||||
func (p *Provider) resolve(ctx context.Context, vmid int) (vmResource, bool, error) {
|
||||
rs, err := p.api.ClusterResources(ctx)
|
||||
if err != nil {
|
||||
return vmResource{}, false, fmt.Errorf("listing cluster resources: %w", err)
|
||||
}
|
||||
for _, r := range rs {
|
||||
if r.VMID == vmid && r.Type == "qemu" {
|
||||
return r, true, nil
|
||||
}
|
||||
}
|
||||
return vmResource{}, false, nil
|
||||
}
|
||||
Reference in New Issue
Block a user