355 lines
14 KiB
Go
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()
|
|
}
|