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, or the VM is too new to be reported yet", 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 guestRunning(st.Status, 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 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 }