Files
forge-tools-proxmox/internal/pve/api.go
T

355 lines
14 KiB
Go

package pve
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/url"
"time"
)
// api.go — типизированные обёртки над Proxmox VE REST (api2/json).
// Чтения возвращают raw-тело "data"; мутации — UPID (асинхронная задача),
// чтобы хендлер вернул идентификатор, а завершение опрашивали через
// task_status(wait) — так мы не блокируем единственный loop (§9.6).
//
// Путь идёт через joinURL (PathEscape), а значения идентификаторов перед
// этим ещё валидируются guard'ом в tools-слое — двойная защита.
// Version — версия PVE в кластере.
func (t *Tenant) Version(ctx context.Context, alias string) (json.RawMessage, error) {
return t.GET(ctx, alias, "/version")
}
// ClusterStatus — состояние кластера (quorum, узлы, версии).
func (t *Tenant) ClusterStatus(ctx context.Context, alias string) (json.RawMessage, error) {
return t.GET(ctx, alias, "/cluster/status")
}
// ClusterResources — ресурсы кластера по PVE-типу (vm/storage/node/sdn).
func (t *Tenant) ClusterResources(ctx context.Context, alias, rtype string) (json.RawMessage, error) {
path := "/cluster/resources"
if rtype != "" {
path += "?type=" + url.QueryEscape(rtype)
}
return t.GET(ctx, alias, path)
}
// GuestResources — VM/CT по типу (qemu|lxc): тянет type=vm и фильтрует по
// полю "type" каждой записи (PVE не принимает type=qemu напрямую).
func (t *Tenant) GuestResources(ctx context.Context, alias, kind string) (json.RawMessage, error) {
raw, err := t.GET(ctx, alias, "/cluster/resources?type=vm")
if err != nil {
return nil, err
}
var arr []map[string]any
if err := json.Unmarshal(raw, &arr); err != nil {
return raw, nil
}
out := make([]map[string]any, 0, len(arr))
for _, it := range arr {
if it["type"] == kind {
out = append(out, it)
}
}
return json.Marshal(out)
}
// Nodes — список нод кластера.
func (t *Tenant) Nodes(ctx context.Context, alias string) (json.RawMessage, error) {
return t.GET(ctx, alias, "/nodes")
}
// NodeStatus — детальное состояние ноды.
func (t *Tenant) NodeStatus(ctx context.Context, alias, node string) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/status"))
}
// NodeNetwork — сетевые интерфейсы ноды (мосты/eth).
func (t *Tenant) NodeNetwork(ctx context.Context, alias, node string) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/network"))
}
// VMConfig — конфиг QEMU-ВМ.
func (t *Tenant) VMConfig(ctx context.Context, alias, node string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/config"))
}
// VMStatus — текущий статус QEMU-ВМ.
func (t *Tenant) VMStatus(ctx context.Context, alias, node string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/status/current"))
}
// ContainerConfig — конфиг LXC.
func (t *Tenant) ContainerConfig(ctx context.Context, alias, node string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/lxc", itoa(vmid), "/config"))
}
// ContainerStatus — текущий статус LXC.
func (t *Tenant) ContainerStatus(ctx context.Context, alias, node string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/lxc", itoa(vmid), "/status/current"))
}
// StorageList — хранилища кластера.
func (t *Tenant) StorageList(ctx context.Context, alias string) (json.RawMessage, error) {
return t.GET(ctx, alias, "/storage")
}
// NodeStorage — хранилища конкретной ноды.
func (t *Tenant) NodeStorage(ctx context.Context, alias, node string) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/storage"))
}
// StorageContent — содержимое хранилища (iso/vztmpl/backup/images).
func (t *Tenant) StorageContent(ctx context.Context, alias, node, storage, contentType string) (json.RawMessage, error) {
path := joinURL("/nodes", node, "/storage", storage, "/content")
if contentType != "" {
path += "?content=" + url.QueryEscape(contentType)
}
return t.GET(ctx, alias, path)
}
// SnapshotList — снапшоты VM/CT.
func (t *Tenant) SnapshotList(ctx context.Context, alias, node, vmtype string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/snapshot"))
}
// BackupList — задачи vzdump (по ноде/всему кластеру).
func (t *Tenant) BackupList(ctx context.Context, alias, node string) (json.RawMessage, error) {
if node != "" {
return t.GET(ctx, alias, joinURL("/nodes", node, "/tasks")+"?typefilter=vzdump")
}
return t.GET(ctx, alias, "/cluster/tasks?typefilter=vzdump")
}
// TasksList — последние задачи кластера.
func (t *Tenant) TasksList(ctx context.Context, alias, node string, limit int) (json.RawMessage, error) {
if node != "" {
return t.GET(ctx, alias, joinURL("/nodes", node, "/tasks")+"?limit="+itoa(limit))
}
return t.GET(ctx, alias, "/cluster/tasks?limit="+itoa(limit))
}
// TaskStatus — статус задачи по UPID.
func (t *Tenant) TaskStatus(ctx context.Context, alias, node, upid string) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/tasks", upid, "/status"))
}
// TaskLog — лог задачи по UPID.
func (t *Tenant) TaskLog(ctx context.Context, alias, node, upid string, limit int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/tasks", upid, "/log")+"?limit="+itoa(limit))
}
// GuestIPs — IP-адреса гостя через QEMU guest-agent (только QEMU, agent:1).
func (t *Tenant) GuestIPs(ctx context.Context, alias, node string, vmid int) (json.RawMessage, error) {
return t.GET(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/agent/network-get-interfaces"))
}
// --- Мутации → возвращают UPID или пустую строку (у части POST нет upid) ---
// VMStart / VMStop / VMReboot / VMShutdown / VMSuspend / VMResume — lifecycle.
// graceful может быть 0 (обычный), 1/2/... для shutdown. Здесь передаём action.
func (t *Tenant) VMAction(ctx context.Context, alias, node string, vmid int, action string, form url.Values) (string, error) {
if form == nil {
form = url.Values{}
}
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/status", action), form)
}
// ContainerAction — lifecycle LXC (start/stop/reboot/shutdown).
func (t *Tenant) ContainerAction(ctx context.Context, alias, node string, vmid int, action string) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/lxc", itoa(vmid), "/status", action), nil)
}
// VMClone — клонирование из template/vmid.
func (t *Tenant) VMClone(ctx context.Context, alias, node string, vmid, newid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/clone"), form)
}
// ContainerClone — клонирование LXC.
func (t *Tenant) ContainerClone(ctx context.Context, alias, node string, vmid, newid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/lxc", itoa(vmid), "/clone"), form)
}
// VMDelete — удаление QEMU-ВМ.
func (t *Tenant) VMDelete(ctx context.Context, alias, node string, vmid int) (string, error) {
return t.DELETEUPID(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid)))
}
// ContainerDelete — удаление LXC.
func (t *Tenant) ContainerDelete(ctx context.Context, alias, node string, vmid int) (string, error) {
return t.DELETEUPID(ctx, alias, joinURL("/nodes", node, "/lxc", itoa(vmid)))
}
// VMConvertTemplate — превращает ВМ в шаблон.
func (t *Tenant) VMConvertTemplate(ctx context.Context, alias, node string, vmid int) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/template"), nil)
}
// NextID — следующий свободный VMID в кластере.
func (t *Tenant) NextID(ctx context.Context, alias string) (json.RawMessage, error) {
return t.GET(ctx, alias, "/cluster/nextid")
}
// SnapshotCreate / SnapshotDelete / SnapshotRollback — снапшоты VM/CT.
func (t *Tenant) SnapshotCreate(ctx context.Context, alias, node, vmtype string, vmid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/snapshot"), form)
}
func (t *Tenant) SnapshotDelete(ctx context.Context, alias, node, vmtype string, vmid int, name string) (string, error) {
return t.DELETEUPID(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/snapshot", name))
}
func (t *Tenant) SnapshotRollback(ctx context.Context, alias, node, vmtype string, vmid int, name string) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/snapshot", name, "/rollback"), nil)
}
// BackupCreate — запускает vzdump (async UPID).
func (t *Tenant) BackupCreate(ctx context.Context, alias, node, vmtype string, vmid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/vzdump"), withBackupForm(form, vmtype, vmid))
}
// BackupRestore (vzdump restore) реализуется через clone из backup volume;
// в v1 это отдельный инструмент, см. tools/backup.go.
// TaskStop — отменяет/стирает задачу.
func (t *Tenant) TaskStop(ctx context.Context, alias, node, upid string) (string, error) {
return t.DELETEUPID(ctx, alias, joinURL("/nodes", node, "/tasks", upid))
}
// ConfigPost — POST на /config (VM/CT): изменение конфига, включая
// добавление/удаление дисков и сетевых интерфейсов (структурный путь).
func (t *Tenant) ConfigPost(ctx context.Context, alias, node, vmtype string, vmid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/config"), form)
}
// ResizeDisk — изменение размера диска VM/CT (/resize).
func (t *Tenant) ResizeDisk(ctx context.Context, alias, node, vmtype string, vmid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, vmtype, itoa(vmid), "/resize"), form)
}
// MoveDisk — перенос диска (storage-миграция) на другую подсистему хранения.
func (t *Tenant) MoveDisk(ctx context.Context, alias, node string, vmid int, form url.Values) (string, error) {
return t.POSTUPID(ctx, alias, joinURL("/nodes", node, "/qemu", itoa(vmid), "/move_disk"), form)
}
// TaskWait ожидает завершения задачи по UPID (с капом task_poll_max_sec),
// возвращая финальный статус. Poll строго bounded (§9.6) — не блокируем
// loop дольше капа даже для долгой операции.
func (t *Tenant) TaskWait(ctx context.Context, alias, node, upid string) (string, error) {
capSec := t.cfg.TaskPollMaxSec
if capSec <= 0 {
capSec = DefaultTaskPollMaxSec
}
deadline := time.Now().Add(time.Duration(capSec) * time.Second)
for {
status, err := t.TaskStatus(ctx, alias, node, upid)
if err != nil {
return "", err
}
done := taskIsDone(status)
if done {
return pretty(json.RawMessage(status)), nil
}
if time.Now().After(deadline) {
return "", fmt.Errorf("task %s still running after %ds (give timeout)", upid, capSec)
}
if !sleep(ctx, 2*time.Second) {
return "", fmt.Errorf("task wait interrupted: %w", ctx.Err())
}
}
}
// taskIsDone сообщает, завершилась ли задача (по статусам PVE).
func taskIsDone(raw json.RawMessage) bool {
var obj struct {
Status string `json:"status"`
}
if err := json.Unmarshal(raw, &obj); err != nil {
return true
}
switch obj.Status {
case "stopped", "failed", "error": // completed (либо упала)
return true
}
return false
}
// --- низкоуровневый доступ (GET/POST/DELETE) ---
// GET — чтение "data".
func (t *Tenant) GET(ctx context.Context, alias, path string) (json.RawMessage, error) {
c, err := t.Client(ctx, alias)
if err != nil {
return nil, err
}
return c.Get(ctx, path)
}
// POSTUPID — POST и возврат UPID из ответа ("upid" либо null).
func (t *Tenant) POSTUPID(ctx context.Context, alias, path string, form url.Values) (string, error) {
c, err := t.Client(ctx, alias)
if err != nil {
return "", err
}
data, err := c.Post(ctx, path, form)
if err != nil {
return "", err
}
return upidOf(data), nil
}
// DELETEUPID — DELETE и возврат UPID из ответа.
func (t *Tenant) DELETEUPID(ctx context.Context, alias, path string) (string, error) {
c, err := t.Client(ctx, alias)
if err != nil {
return "", err
}
data, err := c.Delete(ctx, path)
if err != nil {
return "", err
}
return upidOf(data), nil
}
// upidOf вытаскивает "upid" из JSON-объекта данных (у части POST его нет).
func upidOf(data json.RawMessage) string {
var obj struct {
UPID string `json:"upid"`
}
if err := json.Unmarshal(data, &obj); err == nil && obj.UPID != "" {
return obj.UPID
}
return ""
}
// withBackupForm добавляет типы/цель в form vzdump.
func withBackupForm(form url.Values, vmtype string, vmid int) url.Values {
if form == nil {
form = url.Values{}
}
if vmtype == "qemu" {
form.Set("vmid", itoa(vmid))
form.Set("mode", "snapshot")
} else {
form.Set("vmid", itoa(vmid))
form.Set("mode", "suspend")
}
return form
}
func itoa(i int) string { return fmt.Sprintf("%d", i) }
// pretty форматирует raw JSON для отдачи модели.
func pretty(raw json.RawMessage) string {
if len(raw) == 0 || string(raw) == "null" {
return "(no data)"
}
var b bytes.Buffer
if err := json.Indent(&b, raw, "", " "); err != nil {
return string(raw)
}
return b.String()
}