/** * `celilo proxmox vm list` / `celilo proxmox ct list` — celilo-provisioned * Proxmox instances with their **desired** size (canonical `module_systems` * state, ISS-0150) next to their **actual** size (live from the Proxmox API). A * desired≠actual row means a resize is pending a reconcile/redeploy. * * Read-only sibling of `proxmox node list`; the foundation the `resize` verb * (ISS-0150 P2) builds on. See openspec/changes/proxmox-capacity-lifecycle/. */ import { ProxmoxClient, type ProxmoxClusterResource, type ProxmoxCredentials, } from '../../api-clients/proxmox'; import { getDb } from '../../db/client'; import { getServiceCredentials, listContainerServices } from '../../services/container-service'; import { type ProvisionedSystem, getProvisionedSystems } from '../../services/deployed-systems'; import { celiloIntro } from '../prompts'; import type { CommandResult } from '../types'; import { resolveProxmoxService } from './proxmox-service'; const BYTES_PER_MB = 1024 * 1024; const BYTES_PER_GB = 1024 * 1024 * 1024; export type InstanceKind = 'vm' | 'ct'; /** Proxmox `type` for each celilo instance kind. */ const PROXMOX_TYPE: Record = { vm: 'qemu', ct: 'lxc' }; export interface InstanceRow { module: string; name: string; vmid: number; node: string; status: string; desired: { cpu: number | null; memMb: number | null; diskGb: number | null }; /** null when the instance is in module_systems but not (yet) on the cluster. */ actual: { cpu: number; memMb: number; diskGb: number } | null; } /** * Pure join of celilo's provisioned systems against live Proxmox guests, for one * instance kind. Keyed by vmid; the Proxmox guest `type` decides vm vs ct * membership (a celilo system row doesn't itself record the kind). */ export function joinInstanceRows( systems: ProvisionedSystem[], resources: ProxmoxClusterResource[], kind: InstanceKind, ): InstanceRow[] { const wantType = PROXMOX_TYPE[kind]; const byVmid = new Map(); for (const r of resources) { if (r.type === wantType && typeof r.vmid === 'number') byVmid.set(r.vmid, r); } const rows: InstanceRow[] = []; for (const s of systems) { if (s.vmid == null) continue; const guest = byVmid.get(s.vmid); if (!guest) continue; // not a guest of this kind (other kind, or gone from cluster) rows.push({ module: s.moduleId, name: s.name, vmid: s.vmid, node: guest.node ?? '—', status: guest.status ?? '—', desired: { cpu: s.cpu, memMb: s.memory, diskGb: s.disk }, actual: guest.maxcpu != null && guest.maxmem != null ? { cpu: guest.maxcpu, memMb: Math.round(guest.maxmem / BYTES_PER_MB), diskGb: guest.maxdisk != null ? Math.round(guest.maxdisk / BYTES_PER_GB) : 0, } : null, }); } return rows; } function fmtSize(cpu: number | null, memMb: number | null, diskGb: number | null): string { const c = cpu != null ? `${cpu}c` : '—'; const m = memMb != null ? `${(memMb / 1024).toFixed(0)}G` : '—'; const d = diskGb != null ? `${diskGb}G` : '—'; return `${c}/${m}/${d}`; } export async function handleProxmoxInstanceList( kind: InstanceKind, args: string[], ): Promise { celiloIntro(kind === 'vm' ? 'Proxmox VMs' : 'Proxmox containers'); const resolved = resolveProxmoxService(await listContainerServices(), args[0]); if ('error' in resolved) { console.log(`✗ ${resolved.error}`); return { success: false, error: resolved.error }; } const { service } = resolved; const creds = (await getServiceCredentials(service.id)) as ProxmoxCredentials; const result = await new ProxmoxClient(creds).clusterResources(); if (!result.success) { console.log(`✗ Could not reach Proxmox (${service.serviceId}): ${result.message}`); return { success: false, error: result.message }; } const rows = joinInstanceRows(getProvisionedSystems(getDb()), result.data, kind); if (rows.length === 0) { console.log(`No celilo-provisioned ${kind === 'vm' ? 'VMs' : 'containers'} found.`); return { success: true, message: 'none' }; } console.log(`Service: ${service.serviceId} (${service.name})\n`); console.log( 'MODULE VMID NODE STATUS DESIRED(c/m/d) ACTUAL(c/m/d) DRIFT', ); console.log( '────────────────────────────────────────────────────────────────────────────────────', ); for (const r of rows) { const desired = fmtSize(r.desired.cpu, r.desired.memMb, r.desired.diskGb); const actual = r.actual ? fmtSize(r.actual.cpu, r.actual.memMb, r.actual.diskGb) : '—'; // Only flag drift on a real mismatch — a null desired dimension means // "unset / not yet seeded", not a pending resize. const drift = r.actual && ((r.desired.cpu != null && r.desired.cpu !== r.actual.cpu) || (r.desired.memMb != null && r.desired.memMb !== r.actual.memMb) || (r.desired.diskGb != null && r.desired.diskGb !== r.actual.diskGb)) ? '⚠ resize pending' : ''; console.log( `${r.module.padEnd(19)} ${String(r.vmid).padEnd(6)} ${r.node.padEnd(8)} ${r.status.padEnd(9)} ${desired.padEnd(16)} ${actual.padEnd(15)} ${drift}`, ); } console.log(''); return { success: true, message: `${rows.length} ${kind}(s)` }; }