e3e824185f
- template machine-probe : lscpu Model name + nproc, MemTotal, lsblk disques - parseProbe étendu (cpuModel/cpuCores/memoryBytes/disks) + buildRecommendations (KVM/QEMU → vm_guest_tools) ; tests TDD - runProbe persiste cpu/mem/disks dans machine_hardware ; /probe renvoie recommendations - popup Sonde affiche cpu/ram/disks + profils conseillés tsc 0 · 110 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
196 lines
7.3 KiB
TypeScript
196 lines
7.3 KiB
TypeScript
// 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 }[];
|
|
cpuModel: string | null;
|
|
cpuCores: number | null;
|
|
memoryBytes: number | null;
|
|
disks: { name: string; sizeBytes: number }[];
|
|
}
|
|
|
|
export interface CorrectionProposal {
|
|
osFamily: OsFamily;
|
|
machineKind: MachineKind;
|
|
virtualization: string;
|
|
}
|
|
|
|
export interface ProfileRecommendation {
|
|
profileId: string;
|
|
reason: 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:PROBE_CPU===");
|
|
const cpuBlock = section(raw, "===SU:PROBE_CPU===", "===SU:PROBE_MEM===");
|
|
const memBlock = section(raw, "===SU:PROBE_MEM===", "===SU:PROBE_DISK===");
|
|
const diskBlock = section(raw, "===SU:PROBE_DISK===", "===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] });
|
|
}
|
|
}
|
|
|
|
const cpuModelMatch = /^MODEL=(.+)$/m.exec(cpuBlock);
|
|
const coresMatch = /^\s*(\d+)\s*$/m.exec(cpuBlock);
|
|
const memMatch = /^MemTotal:\s+(\d+)\s*kB/m.exec(memBlock);
|
|
|
|
const disks: ProbeResult["disks"] = [];
|
|
for (const line of diskBlock.split("\n")) {
|
|
if (!line.startsWith("DISK\t")) continue;
|
|
const [, name, size] = line.split("\t");
|
|
if (name) disks.push({ name, sizeBytes: Number(size) || 0 });
|
|
}
|
|
|
|
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,
|
|
cpuModel: cpuModelMatch?.[1]?.trim() || null,
|
|
cpuCores: coresMatch?.[1] ? Number(coresMatch[1]) : null,
|
|
memoryBytes: memMatch?.[1] ? Number(memMatch[1]) * 1024 : null,
|
|
disks,
|
|
};
|
|
}
|
|
|
|
/** Recommandations de profils post-install déduites de la sonde. */
|
|
export function buildRecommendations(p: ProbeResult): ProfileRecommendation[] {
|
|
const recs: ProfileRecommendation[] = [];
|
|
if (p.virt === "kvm" || p.virt === "qemu") {
|
|
recs.push({ profileId: "vm_guest_tools", reason: "QEMU/KVM détecté → qemu-guest-agent" });
|
|
} else if (p.virt === "vmware") {
|
|
recs.push({ profileId: "vm_guest_tools", reason: "VMware détecté → open-vm-tools" });
|
|
}
|
|
return recs;
|
|
}
|
|
|
|
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;
|
|
recommendations: ProfileRecommendation[];
|
|
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<ProbeOutcome> {
|
|
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();
|
|
const hwFields = {
|
|
cpuModel: probe.cpuModel,
|
|
cpuCores: probe.cpuCores,
|
|
memoryBytes: probe.memoryBytes,
|
|
disksJson: JSON.stringify(probe.disks),
|
|
gpusJson: JSON.stringify(probe.gpus),
|
|
networkJson: JSON.stringify(probe.net),
|
|
updatedAt: now,
|
|
};
|
|
db.insert(schema.machineHardware)
|
|
.values({ machineId, ...hwFields })
|
|
.onConflictDoUpdate({ target: schema.machineHardware.machineId, set: hwFields })
|
|
.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, recommendations: buildRecommendations(probe), raw, changes };
|
|
}
|