package proxmox import "encoding/json" // Types mirror the exact JSON shapes captured from the live demo host // (demo-felhom, PVE 9.2.2, 2026-06-08) via `pvesh get ... --output-format json`. // Decoding ignores unknown fields, so we depend only on the fields we use. // Version is GET /version. type Version struct { Release string `json:"release"` // "9.2" RepoID string `json:"repoid"` Version string `json:"version"` // "9.2.2" } // Node is one entry of GET /nodes. type Node struct { Node string `json:"node"` // node name, e.g. "demo-felhom" Status string `json:"status"` // "online" CPU float64 `json:"cpu"` // load fraction 0..1 MaxCPU int `json:"maxcpu"` Mem int64 `json:"mem"` MaxMem int64 `json:"maxmem"` Disk int64 `json:"disk"` MaxDisk int64 `json:"maxdisk"` Uptime int64 `json:"uptime"` SSLFingerprint string `json:"ssl_fingerprint"` } // NodeStatus is GET /nodes/{node}/status (host metrics; needs Sys.Audit). type NodeStatus struct { CPU float64 `json:"cpu"` // load fraction 0..1 Uptime int64 `json:"uptime"` LoadAvg []string `json:"loadavg"` // 1/5/15-min, as strings in the API PVEVersion string `json:"pveversion"` KVersion string `json:"kversion"` Memory struct { Total int64 `json:"total"` Used int64 `json:"used"` Free int64 `json:"free"` Available int64 `json:"available"` } `json:"memory"` RootFS struct { Total int64 `json:"total"` Used int64 `json:"used"` Free int64 `json:"free"` Avail int64 `json:"avail"` } `json:"rootfs"` Swap struct { Total int64 `json:"total"` Used int64 `json:"used"` Free int64 `json:"free"` } `json:"swap"` CPUInfo struct { Cores int `json:"cores"` CPUs int `json:"cpus"` Sockets int `json:"sockets"` Model string `json:"model"` } `json:"cpuinfo"` } // Guest is one entry of GET /nodes/{node}/lxc and the body of // GET /nodes/{node}/lxc/{vmid}/status/current. The status/current response has no // vmid field (it is in the path), so callers set VMID from the request argument. type Guest struct { VMID int `json:"vmid"` Name string `json:"name"` Status string `json:"status"` // "running" | "stopped" Type string `json:"type"` // "lxc" CPUs int `json:"cpus"` CPU float64 `json:"cpu"` Mem int64 `json:"mem"` MaxMem int64 `json:"maxmem"` Disk int64 `json:"disk"` MaxDisk int64 `json:"maxdisk"` Uptime int64 `json:"uptime"` } // PoolInfo is GET /pools/{poolid} — the pool's identity + membership. The stale-lock recovery // uses it as the ownership registry: only pool members are ever scanned (audit A1). type PoolInfo struct { PoolID string `json:"poolid"` Members []PoolMember `json:"members"` } // PoolMember is one entry of PoolInfo.Members. A pool can hold guests AND storages; storage // entries carry type "storage" and no vmid, so membership checks must filter on both (spike // SPIKE-a1-pool-membership-read §8). type PoolMember struct { VMID int `json:"vmid"` Type string `json:"type"` // "lxc" | "qemu" | "storage" } // GuestConfig is GET /nodes/{node}/lxc/{vmid}/config. The config surface is // dynamic (net0..netN, mp0..mpN, unusedN), so known fields are typed and the full // raw map is preserved in Extra for the dynamic ones. type GuestConfig struct { Hostname string `json:"hostname"` Arch string `json:"arch"` Cores int `json:"cores"` Memory int64 `json:"memory"` Swap int64 `json:"swap"` OSType string `json:"ostype"` RootFS string `json:"rootfs"` Features string `json:"features"` // e.g. "nesting=1,keyctl=1" Unprivileged int `json:"unprivileged"` // 1 if unprivileged Digest string `json:"digest"` // Extra holds every field as raw JSON, including the dynamic netN/mpN/unusedN // keys not promoted above. Extra map[string]json.RawMessage `json:"-"` } // UnmarshalJSON fills both the typed known fields and the raw Extra map. func (g *GuestConfig) UnmarshalJSON(b []byte) error { type alias GuestConfig // avoid recursion var a alias if err := json.Unmarshal(b, &a); err != nil { return err } *g = GuestConfig(a) return json.Unmarshal(b, &g.Extra) } // MountPoints returns the mpN entries (e.g. "mp0" -> "local-lvm:1,mp=/mnt/mp1,backup=0") // pulled from Extra. Relevant for later slices' bulk-volume placement. func (g *GuestConfig) MountPoints() map[string]string { return g.prefixed("mp") } // Nets returns the netN entries from Extra. func (g *GuestConfig) Nets() map[string]string { return g.prefixed("net") } // Unused returns the unusedN entries from Extra — the displaced volumes PVE parks in the config // when a mountpoint is overridden/replaced (e.g. the DR bring-up's structural-bind swap replacing // its restore-time throwaway volumes). The caller deletes these so a kept guest carries no residue. func (g *GuestConfig) Unused() map[string]string { return g.prefixed("unused") } // Lock returns the guest's current lock ("backup", "snapshot-delete", "migrate", …) from the config, // or "" when unlocked. An interrupted vzdump leaves a "backup" or "snapshot-delete" lock — the signal // the startup stale-lock recovery (F2-b) keys on. func (g *GuestConfig) Lock() string { raw, ok := g.Extra["lock"] if !ok { return "" } var s string if json.Unmarshal(raw, &s) != nil { return "" } return s } // OnBoot reports whether the guest is configured to auto-start at host boot (onboot:1). PVE omits the // key when 0, so an absent key reads as false. func (g *GuestConfig) OnBoot() bool { raw, ok := g.Extra["onboot"] if !ok { return false } var n int if json.Unmarshal(raw, &n) != nil { return false } return n == 1 } func (g *GuestConfig) prefixed(prefix string) map[string]string { out := map[string]string{} for k, raw := range g.Extra { if len(k) <= len(prefix) || k[:len(prefix)] != prefix { continue } // require the suffix to be a digit (mp0, net0 — not "memory") if c := k[len(prefix)]; c < '0' || c > '9' { continue } var s string if json.Unmarshal(raw, &s) == nil { out[k] = s } } return out } // Snapshot is one entry of GET /nodes/{node}/lxc/{vmid}/snapshot. The list always includes the // synthetic "current" pseudo-snapshot; named entries are real snapshots. An interrupted vzdump // snapshot-mode backup leaves a dangling snapshot named exactly "vzdump". type Snapshot struct { Name string `json:"name"` Description string `json:"description,omitempty"` SnapTime int64 `json:"snaptime,omitempty"` Parent string `json:"parent,omitempty"` } // Storage is one entry of GET /storage (cluster) and GET /nodes/{node}/storage // (the latter adds usage fields). Unused fields stay zero. // // The lower block (Server/Export/Share/Datastore/Fingerprint/VGName/ThinPool) are the // type-specific config fields the cluster /storage definition carries; they are the // source for slice-5's deterministic durable_id derivation (server:export for NFS/CIFS, // repo+fingerprint for PBS, vg/pool for lvmthin). Additive parse-only fields — decoding // ignores unknown keys, so a storage type that lacks one simply leaves it zero. type Storage struct { Storage string `json:"storage"` Type string `json:"type"` // "dir" | "lvmthin" | "nfs" | "cifs" | "pbs" Content string `json:"content"` // comma list, e.g. "vztmpl,backup,iso,import" Path string `json:"path,omitempty"` Total int64 `json:"total,omitempty"` Used int64 `json:"used,omitempty"` Avail int64 `json:"avail,omitempty"` Active int `json:"active,omitempty"` Enabled int `json:"enabled,omitempty"` Shared int `json:"shared,omitempty"` UsedFraction float64 `json:"used_fraction,omitempty"` // Type-specific config (durable_id sources). Server string `json:"server,omitempty"` // nfs/cifs/pbs server host Export string `json:"export,omitempty"` // nfs export path Share string `json:"share,omitempty"` // cifs share name Datastore string `json:"datastore,omitempty"` // pbs datastore name Fingerprint string `json:"fingerprint,omitempty"` // pbs server cert fingerprint Username string `json:"username,omitempty"` // pbs auth id, e.g. "felhom@pbs!n100" Namespace string `json:"namespace,omitempty"` // pbs namespace ("" = root; per-customer tenancy = S4) VGName string `json:"vgname,omitempty"` // lvm/lvmthin volume group ThinPool string `json:"thinpool,omitempty"` // lvmthin pool LV name } // StorageContent is one entry of GET /nodes/{node}/storage/{store}/content // (e.g. vzdump archives, CT templates, guest volumes). type StorageContent struct { VolID string `json:"volid"` // e.g. "local:backup/vzdump-lxc-9001-...tar.zst" Content string `json:"content"` Format string `json:"format"` Size int64 `json:"size"` CTime int64 `json:"ctime"` VMID int `json:"vmid,omitempty"` }