package proxmox import ( "context" "fmt" "net/http" "net/url" "strconv" ) // Async mutating operations. Each is API-token-covered (the FelhomAgent role) and // returns a UPID string; the caller MUST WaitTask on it and assert exitstatus OK. // The HTTP 200 here is not proof of success (phase1-2 §1.3). // BackupMode is the vzdump mode. type BackupMode string const ( // ModeStop: orderly guest shutdown -> backup -> restart. Highest consistency. // For LXC the shutdown/restart is internal to vzdump and needs only VM.Backup // (NOT VM.PowerMgmt) — phase1-2 §1.4. ModeStop BackupMode = "stop" // ModeSnapshot: lowest downtime; for an LXC this is crash-consistent only (no // fsfreeze) — app-consistency is the controller's job (proxmox-platform.md §4.2). ModeSnapshot BackupMode = "snapshot" ) // RestoreLXCOptions parameterizes a restore. This is the PRIMARY create path: // a token-authorized restore preserves features=nesting=1,keyctl=1 from the // archive, so it needs no root (phase3 §B3). Fresh `pct create` with keyctl is // the only root-fenced create (see Privileged.CreateGoldenLXC). type RestoreLXCOptions struct { VMID int // target VMID (fresh id) Archive string // source archive volid, e.g. "local:backup/vzdump-lxc-9001-...tar.zst" Storage string // target storage for the rootfs, e.g. "local-lvm" Force bool // overwrite an existing VMID (destructive — caller must have authority) } // RestoreLXC restores an LXC from a vzdump/PBS archive via POST /nodes/{node}/lxc // (restore=1). Returns the UPID. NOTE: pct restore preserves the source MAC + // hostname — reset network identity before starting alongside the original // (phase1-2 §2.2). Identity reset is a SetConfig call the caller makes after. func (c *Client) RestoreLXC(ctx context.Context, opts RestoreLXCOptions) (string, error) { if opts.VMID == 0 || opts.Archive == "" || opts.Storage == "" { return "", fmt.Errorf("proxmox: RestoreLXC needs vmid, archive and storage") } v := url.Values{} v.Set("vmid", strconv.Itoa(opts.VMID)) v.Set("ostemplate", opts.Archive) // pct restore source v.Set("restore", "1") v.Set("storage", opts.Storage) if opts.Force { v.Set("force", "1") } return c.dataString(ctx, http.MethodPost, "/nodes/"+c.node+"/lxc", v) } // VzdumpOptions parameterizes a backup. type VzdumpOptions struct { VMID int Storage string // a storage whose content includes "backup" (e.g. "local") — NOT local-lvm Mode BackupMode // ModeStop | ModeSnapshot Compress string // "zstd" (default), "lzo", "gzip", or "" for none } // Vzdump starts a backup via POST /nodes/{node}/vzdump. Returns the UPID. An // agent-initiated vzdump is crash-consistent only for an LXC (no fsfreeze). func (c *Client) Vzdump(ctx context.Context, opts VzdumpOptions) (string, error) { if opts.VMID == 0 || opts.Storage == "" || opts.Mode == "" { return "", fmt.Errorf("proxmox: Vzdump needs vmid, storage and mode") } v := url.Values{} v.Set("vmid", strconv.Itoa(opts.VMID)) v.Set("storage", opts.Storage) v.Set("mode", string(opts.Mode)) if opts.Compress == "" { opts.Compress = "zstd" } v.Set("compress", opts.Compress) return c.dataString(ctx, http.MethodPost, "/nodes/"+c.node+"/vzdump", v) } // Snapshot creates an LXC snapshot via POST /nodes/{node}/lxc/{vmid}/snapshot. // A running, unprivileged LXC can be snapshotted on LVM-thin with no stop // (phase1-2 §1.6) — this is the snapshot-before-change primitive. func (c *Client) Snapshot(ctx context.Context, vmid int, snapname, description string) (string, error) { if vmid == 0 || snapname == "" { return "", fmt.Errorf("proxmox: Snapshot needs vmid and snapname") } v := url.Values{} v.Set("snapname", snapname) if description != "" { v.Set("description", description) } path := fmt.Sprintf("/nodes/%s/lxc/%d/snapshot", c.node, vmid) return c.dataString(ctx, http.MethodPost, path, v) } // Rollback rolls an LXC back to a snapshot via // POST /nodes/{node}/lxc/{vmid}/snapshot/{snap}/rollback. func (c *Client) Rollback(ctx context.Context, vmid int, snapname string) (string, error) { if vmid == 0 || snapname == "" { return "", fmt.Errorf("proxmox: Rollback needs vmid and snapname") } path := fmt.Sprintf("/nodes/%s/lxc/%d/snapshot/%s/rollback", c.node, vmid, url.PathEscape(snapname)) return c.dataString(ctx, http.MethodPost, path, url.Values{}) } // DeleteSnapshot removes an LXC snapshot via // DELETE /nodes/{node}/lxc/{vmid}/snapshot/{snap}. func (c *Client) DeleteSnapshot(ctx context.Context, vmid int, snapname string) (string, error) { if vmid == 0 || snapname == "" { return "", fmt.Errorf("proxmox: DeleteSnapshot needs vmid and snapname") } path := fmt.Sprintf("/nodes/%s/lxc/%d/snapshot/%s", c.node, vmid, url.PathEscape(snapname)) return c.dataString(ctx, http.MethodDelete, path, nil) } // SetConfig applies config changes via PUT /nodes/{node}/lxc/{vmid}/config // (e.g. memory, cores, net0, mpN with a backup flag). PVE may apply this // synchronously (no UPID) — the returned string is empty in that case, and "" is // not an error. When a UPID is returned, WaitTask on it. // // Identity reset after a restore (phase1-2 §2.2) is a SetConfig with // params{"net0": "name=eth0,bridge=vmbr0,ip=dhcp"} (regenerates the MAC). func (c *Client) SetConfig(ctx context.Context, vmid int, params map[string]string) (string, error) { if vmid == 0 || len(params) == 0 { return "", fmt.Errorf("proxmox: SetConfig needs vmid and at least one param") } v := url.Values{} for k, val := range params { v.Set(k, val) } path := fmt.Sprintf("/nodes/%s/lxc/%d/config", c.node, vmid) return c.dataString(ctx, http.MethodPut, path, v) } // Start starts a guest via POST /nodes/{node}/lxc/{vmid}/status/start (VM.PowerMgmt). func (c *Client) Start(ctx context.Context, vmid int) (string, error) { path := fmt.Sprintf("/nodes/%s/lxc/%d/status/start", c.node, vmid) return c.dataString(ctx, http.MethodPost, path, url.Values{}) } // Stop stops a guest via POST /nodes/{node}/lxc/{vmid}/status/stop (VM.PowerMgmt). func (c *Client) Stop(ctx context.Context, vmid int) (string, error) { path := fmt.Sprintf("/nodes/%s/lxc/%d/status/stop", c.node, vmid) return c.dataString(ctx, http.MethodPost, path, url.Values{}) }