package proxmox import ( "context" "fmt" "time" "github.com/go-logr/logr" ) // Delete is spec entry 11: under the per-name lock, stop and destroy the VM // when it is ours, then delete its seed. Everything already gone is // success, and nothing of ours is left when nil is returned, so Get // afterwards is NotFound. func (p *Provider) Delete(ctx context.Context, id string) error { vmid, name, err := parseProviderID(id) if err != nil { return p.wrapErr("delete", id, err) } log := p.logger(ctx).WithValues("vmid", vmid, "name", name) unlock, err := p.lockName(ctx, name) if err != nil { return p.wrapErr("delete", id, err) } defer unlock() node, err := p.destroy(ctx, log, vmid, name) if err != nil { return p.wrapErr("delete", id, err) } if node == "" { err = p.deleteSeedEverywhere(ctx, log, name) } else { log = log.WithValues("node", node) err = p.deleteSeed(ctx, log, node, name) } if err != nil { return p.wrapErr("delete", id, err) } log.V(1).Info("proxmox vm deleted", "destroyed", node != "") return nil } // destroy is steps 2 to 6. It returns the node of the VM it destroyed, or // "" when there was no VM of ours to destroy: then the seed's node is not // known, and the seed is looked for on every node. func (p *Provider) destroy(ctx context.Context, log logr.Logger, vmid int, name string) (string, error) { r, ok, err := p.resolve(ctx, vmid) if err != nil { return "", err } if !ok { log.V(1).Info("proxmox vm already gone") return "", nil } log = log.WithValues("node", r.Node) if r.Lock != "" && r.Lock != lockSuspended { return "", transientf("VM %d is locked (%s)", vmid, r.Lock) } if r.Status == "unknown" { return "", 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 isNotExist(err) { log.V(1).Info("proxmox vm already gone") return "", nil } if err != nil { return "", fmt.Errorf("reading config of VM %d: %w", vmid, err) } if cfg["name"] != name { log.V(1).Info("proxmox vm at this vmid is not ours, left alone", "vmName", cfg["name"]) return "", nil } if !parseDescription(cfg["description"]).Managed { log.V(1).Info("proxmox vm carries no proxy-operator mark, left alone") return "", nil } switch lock := cfg["lock"]; lock { case "": case lockSuspended: // Destroy refuses a locked VM and the lock never clears on its own, // so only an admin can move this on, as in Create's ownership check. return "", permanentf("VM %d named %s is hibernated (lock suspended); resume or destroy it in Proxmox", vmid, name) default: return "", transientf("VM %d is locked (%s)", vmid, lock) } // The listing shows a paused VM as running, and Stop ends it too. if r.Status != "stopped" { gone, err := p.runTask(ctx, log, "stopping", vmid, p.budgets.stop, func() (string, error) { return p.api.Stop(ctx, r.Node, vmid) }) if err != nil || gone { return r.Node, err } } if _, err := p.runTask(ctx, log, "destroying", vmid, p.budgets.destroy, func() (string, error) { return p.api.Destroy(ctx, r.Node, vmid) }); err != nil { return "", err } return r.Node, nil } // runTask submits a task on a VM and waits for it. A VM that is gone by the // time the task is submitted reports gone: it is what the task was for. func (p *Provider) runTask(ctx context.Context, log logr.Logger, what string, vmid int, budget time.Duration, submit func() (string, error)) (bool, error) { upid, err := submit() if isNotExist(err) { log.V(1).Info("proxmox vm already gone") return true, nil } if err != nil { return false, fmt.Errorf("%s VM %d: %w", what, vmid, err) } if err := p.waitTask(ctx, log, upid, budget); err != nil { return false, fmt.Errorf("%s VM %d: %w", what, vmid, err) } return false, nil } // deleteSeed is step 7 on one node. A missing volume still ends in a // successful task (F13), so nothing is matched. func (p *Provider) deleteSeed(ctx context.Context, log logr.Logger, node, name string) error { volid := seedVolID(p.cfg.ISOStorage, name) upid, err := p.api.DeleteVolume(ctx, node, p.cfg.ISOStorage, volid) if err != nil { return fmt.Errorf("deleting seed %s on %s: %w", volid, node, err) } if err := p.waitTask(ctx, log, upid, p.budgets.seedDelete); err != nil { return fmt.Errorf("deleting seed %s on %s: %w", volid, node, err) } return nil } // deleteSeedEverywhere is step 7 when the VM's node is not known: every // configured node is tried, and one that fails is logged and skipped, so a // node that is down or lacks the storage cannot hold the Delete up. An // ended ctx fails the call instead. func (p *Provider) deleteSeedEverywhere(ctx context.Context, log logr.Logger, name string) error { for _, node := range p.cfg.Nodes { err := p.deleteSeed(ctx, log, node, name) if err == nil { continue } if ctx.Err() != nil { return err } log.V(1).Info("proxmox seed delete failed on a node, skipped", "node", node, "reason", err.Error()) } return nil }