// server/services/machineProbe.ts import { eq } from "drizzle-orm"; import { db, schema } from "../db/client.js"; import { getMachineRow, getCreds } from "./machines.js"; import { renderTemplate } from "../templates/render.js"; import { runScriptSudo } from "../ssh/client.js"; import { outputHub } from "../ws/outputHub.js"; import type { OsFamily, MachineKind } from "@shared/types.js"; // ---------------------------------------------------------------------------- // Fonctions pures (testables). // ---------------------------------------------------------------------------- export interface ProbeResult { osId: string | null; osVersion: string | null; osCodename: string | null; arch: string | null; dpkgArch: string | null; virt: string | null; isProxmox: boolean; isRpi: boolean; gpus: string[]; net: { iface: string; addr: string }[]; } export interface CorrectionProposal { osFamily: OsFamily; machineKind: MachineKind; virtualization: string; } function section(raw: string, start: string, end?: string): string { const i = raw.indexOf(start); if (i < 0) return ""; const from = i + start.length; const j = end ? raw.indexOf(end, from) : -1; return raw.slice(from, j < 0 ? undefined : j).trim(); } function osReleaseValue(block: string, key: string): string | null { const m = new RegExp(`^${key}=(.*)$`, "m").exec(block); if (!m || m[1] === undefined) return null; return m[1].replace(/^"(.*)"$/, "$1").trim() || null; } export function parseProbe(raw: string): ProbeResult { const os = section(raw, "===SU:PROBE_OS===", "===SU:PROBE_ARCH==="); const archBlock = section(raw, "===SU:PROBE_ARCH===", "===SU:PROBE_VIRT===").split("\n"); const virt = section(raw, "===SU:PROBE_VIRT===", "===SU:PROBE_PROXMOX===").split("\n")[0]?.trim() || null; const prox = section(raw, "===SU:PROBE_PROXMOX===", "===SU:PROBE_RPI==="); const rpi = section(raw, "===SU:PROBE_RPI===", "===SU:PROBE_GPU==="); const gpuBlock = section(raw, "===SU:PROBE_GPU===", "===SU:PROBE_NET==="); const netBlock = section(raw, "===SU:PROBE_NET===", "===SU:EXIT="); const gpus = gpuBlock .split("\n") .map((l) => l.trim()) .filter((l) => l && l !== "no-lspci"); const net: ProbeResult["net"] = []; for (const line of netBlock.split("\n")) { const parts = line.trim().split(/\s+/); if (parts.length >= 2 && parts[0] && parts[1] && parts[0] !== "lo") { net.push({ iface: parts[0], addr: parts[1] }); } } return { osId: osReleaseValue(os, "ID"), osVersion: osReleaseValue(os, "VERSION_ID"), osCodename: osReleaseValue(os, "VERSION_CODENAME"), arch: archBlock[0]?.trim() || null, dpkgArch: archBlock[1]?.trim() || null, virt, isProxmox: /PROXMOX=1/.test(prox), isRpi: /RPI=1/.test(rpi), gpus, net, }; } const VM_VIRTS = new Set(["kvm", "qemu", "vmware", "oracle", "microsoft", "xen", "bochs", "parallels"]); const LXC_VIRTS = new Set(["lxc", "lxc-libvirt", "openvz", "systemd-nspawn", "docker", "podman"]); export function proposeCorrections(p: ProbeResult): CorrectionProposal { const virtualization = p.virt && p.virt !== "none" ? p.virt : "none"; let osFamily: OsFamily; if (p.isProxmox) osFamily = "proxmox"; else if (p.isRpi) osFamily = "raspbian"; else if (p.osId === "ubuntu") osFamily = "ubuntu"; else if (p.osId === "debian" || p.osId === "raspbian") osFamily = "debian"; else osFamily = "unknown"; let machineKind: MachineKind; if (p.isProxmox) machineKind = "proxmox_host"; else if (p.isRpi) machineKind = "raspberry_pi"; else if (p.virt && VM_VIRTS.has(p.virt)) machineKind = "vm"; else if (p.virt && LXC_VIRTS.has(p.virt)) machineKind = "lxc"; else if (p.virt === "none") machineKind = "physical"; else machineKind = "unknown"; return { osFamily, machineKind, virtualization }; } // ---------------------------------------------------------------------------- // Orchestration (SSH, lecture seule). Persiste les faits matériels ; ne corrige PAS // os_family/machine_kind automatiquement — la proposition est renvoyée pour validation. // ---------------------------------------------------------------------------- export interface ProbeOutcome { probe: ProbeResult; proposal: CorrectionProposal; raw: string; changes: string[]; // diff entre l'actuel et la proposition (pour l'UI) } export async function runProbe(machineId: string, onData?: (c: string) => void): Promise { const m = getMachineRow(machineId); if (!m) throw new Error("Machine introuvable"); const script = renderTemplate("apt/machine-probe.sh.tpl", {}); const res = await runScriptSudo(getCreds(m), script, (c) => { onData?.(c); outputHub.publish(machineId, c); }); const raw = res.stdout; const probe = parseProbe(raw); const proposal = proposeCorrections(probe); const now = new Date().toISOString(); db.insert(schema.machineHardware) .values({ machineId, gpusJson: JSON.stringify(probe.gpus), networkJson: JSON.stringify(probe.net), updatedAt: now, }) .onConflictDoUpdate({ target: schema.machineHardware.machineId, set: { gpusJson: JSON.stringify(probe.gpus), networkJson: JSON.stringify(probe.net), updatedAt: now }, }) .run(); const changes: string[] = []; if (proposal.osFamily !== m.osFamily) changes.push(`os_family: ${m.osFamily} → ${proposal.osFamily}`); if (proposal.machineKind !== (m.machineKind ?? "unknown")) { changes.push(`machine_kind: ${m.machineKind ?? "—"} → ${proposal.machineKind}`); } if (proposal.virtualization !== (m.virtualization ?? "none")) { changes.push(`virtualization: ${m.virtualization ?? "—"} → ${proposal.virtualization}`); } return { probe, proposal, raw, changes }; }