diff --git a/client/src/features/machines/MachineTile.tsx b/client/src/features/machines/MachineTile.tsx
index 031446c..9488c02 100644
--- a/client/src/features/machines/MachineTile.tsx
+++ b/client/src/features/machines/MachineTile.tsx
@@ -296,10 +296,24 @@ function MachineConfigPopup({
{probe.probe.isProxmox ? " · proxmox" : ""}
{probe.probe.isRpi ? " · rpi" : ""}
+ {(probe.probe.cpuModel || probe.probe.memoryBytes) && (
+
+ cpu={probe.probe.cpuModel ?? "?"} ({probe.probe.cpuCores ?? "?"}c) · ram=
+ {probe.probe.memoryBytes ? `${(probe.probe.memoryBytes / 1e9).toFixed(1)} Go` : "?"} · disks=
+ {probe.probe.disks.length}
+
+ )}
proposition : os_family={probe.proposal.osFamily} · machine_kind={probe.proposal.machineKind} · virt=
{probe.proposal.virtualization}
+ {probe.recommendations.length > 0 && (
+
+ {probe.recommendations.map((r, i) => (
+ - ↪ profil conseillé : {r.profileId} — {r.reason}
+ ))}
+
+ )}
{probe.changes.length ? (
<>
diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts
index da4ac61..744a01e 100644
--- a/client/src/lib/api.ts
+++ b/client/src/lib/api.ts
@@ -208,8 +208,13 @@ export interface ProbeResultView {
isRpi: boolean;
gpus: string[];
net: { iface: string; addr: string }[];
+ cpuModel: string | null;
+ cpuCores: number | null;
+ memoryBytes: number | null;
+ disks: { name: string; sizeBytes: number }[];
};
proposal: { osFamily: OsFamily; machineKind: MachineKind; virtualization: string };
+ recommendations: { profileId: string; reason: string }[];
changes: string[];
}
diff --git a/server/routes/machines.ts b/server/routes/machines.ts
index 70fa1b1..3851bee 100644
--- a/server/routes/machines.ts
+++ b/server/routes/machines.ts
@@ -79,7 +79,7 @@ machinesRoutes.get("/:id/hardware", (c) => {
machinesRoutes.post("/:id/probe", async (c) => {
try {
const o = await runProbe(c.req.param("id"));
- return c.json({ probe: o.probe, proposal: o.proposal, changes: o.changes });
+ return c.json({ probe: o.probe, proposal: o.proposal, recommendations: o.recommendations, changes: o.changes });
} catch (err) {
return c.json({ error: (err as Error).message }, 400);
}
diff --git a/server/services/machineProbe.test.ts b/server/services/machineProbe.test.ts
index c62ea12..c851cf7 100644
--- a/server/services/machineProbe.test.ts
+++ b/server/services/machineProbe.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
-import { parseProbe, proposeCorrections } from "./machineProbe.js";
+import { parseProbe, proposeCorrections, buildRecommendations } from "./machineProbe.js";
const PROXMOX = [
"===SU:PROBE_OS===",
@@ -101,3 +101,46 @@ describe("proposeCorrections", () => {
expect(c.virtualization).toBe("kvm");
});
});
+
+describe("sonde enrichie (cpu/mem/disk + recommandations)", () => {
+ const ENRICHED = [
+ "===SU:PROBE_OS===",
+ "ID=debian",
+ "===SU:PROBE_ARCH===",
+ "x86_64",
+ "amd64",
+ "===SU:PROBE_VIRT===",
+ "kvm",
+ "===SU:PROBE_PROXMOX===",
+ "PROXMOX=0",
+ "===SU:PROBE_RPI===",
+ "RPI=0",
+ "===SU:PROBE_GPU===",
+ "no-lspci",
+ "===SU:PROBE_NET===",
+ "ens18 10.0.0.8/22",
+ "===SU:PROBE_CPU===",
+ "MODEL=Intel(R) Xeon(R) CPU E5-2670",
+ "4",
+ "===SU:PROBE_MEM===",
+ "MemTotal: 4194304 kB",
+ "===SU:PROBE_DISK===",
+ "DISK\tsda\t34359738368",
+ "DISK\tsdb\t1073741824000",
+ "===SU:EXIT=0===",
+ ].join("\n");
+
+ it("extrait cpuModel/cores, mémoire et disques", () => {
+ const p = parseProbe(ENRICHED);
+ expect(p.cpuModel).toBe("Intel(R) Xeon(R) CPU E5-2670");
+ expect(p.cpuCores).toBe(4);
+ expect(p.memoryBytes).toBe(4194304 * 1024);
+ expect(p.disks).toHaveLength(2);
+ expect(p.disks[0]).toEqual({ name: "sda", sizeBytes: 34359738368 });
+ });
+
+ it("recommande vm_guest_tools sur KVM", () => {
+ const recs = buildRecommendations(parseProbe(ENRICHED));
+ expect(recs.some((r) => r.profileId === "vm_guest_tools")).toBe(true);
+ });
+});
diff --git a/server/services/machineProbe.ts b/server/services/machineProbe.ts
index b49fc54..3e792ad 100644
--- a/server/services/machineProbe.ts
+++ b/server/services/machineProbe.ts
@@ -22,6 +22,10 @@ export interface ProbeResult {
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 {
@@ -30,6 +34,11 @@ export interface CorrectionProposal {
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 "";
@@ -51,7 +60,10 @@ export function parseProbe(raw: string): ProbeResult {
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 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")
@@ -66,6 +78,17 @@ export function parseProbe(raw: string): ProbeResult {
}
}
+ 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"),
@@ -77,9 +100,24 @@ export function parseProbe(raw: string): ProbeResult {
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"]);
@@ -112,6 +150,7 @@ export function proposeCorrections(p: ProbeResult): CorrectionProposal {
export interface ProbeOutcome {
probe: ProbeResult;
proposal: CorrectionProposal;
+ recommendations: ProfileRecommendation[];
raw: string;
changes: string[]; // diff entre l'actuel et la proposition (pour l'UI)
}
@@ -129,17 +168,18 @@ export async function runProbe(machineId: string, onData?: (c: string) => void):
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,
- 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 },
- })
+ .values({ machineId, ...hwFields })
+ .onConflictDoUpdate({ target: schema.machineHardware.machineId, set: hwFields })
.run();
const changes: string[] = [];
@@ -151,5 +191,5 @@ export async function runProbe(machineId: string, onData?: (c: string) => void):
changes.push(`virtualization: ${m.virtualization ?? "—"} → ${proposal.virtualization}`);
}
- return { probe, proposal, raw, changes };
+ return { probe, proposal, recommendations: buildRecommendations(probe), raw, changes };
}
diff --git a/templates/apt/machine-probe.sh.tpl b/templates/apt/machine-probe.sh.tpl
index 7d6fd8c..996577c 100644
--- a/templates/apt/machine-probe.sh.tpl
+++ b/templates/apt/machine-probe.sh.tpl
@@ -17,4 +17,11 @@ echo "===SU:PROBE_GPU==="
command -v lspci >/dev/null 2>&1 && lspci 2>/dev/null | grep -Ei 'vga|3d|display' || echo "no-lspci"
echo "===SU:PROBE_NET==="
ip -o -4 addr show 2>/dev/null | awk '{print $2, $4}'
+echo "===SU:PROBE_CPU==="
+LANG=C lscpu 2>/dev/null | grep -E '^Model name:' | sed 's/^Model name:[[:space:]]*/MODEL=/' || true
+nproc 2>/dev/null
+echo "===SU:PROBE_MEM==="
+grep -E '^MemTotal:' /proc/meminfo 2>/dev/null
+echo "===SU:PROBE_DISK==="
+lsblk -b -d -n -o NAME,TYPE,SIZE 2>/dev/null | awk '$2=="disk"{print "DISK\t"$1"\t"$3}'
echo "===SU:EXIT=0==="