feat(os): profils Proxmox/RPi + machine_probe + proxy persistent (tâche 2 SJ-7)

- templates proxmox/ (update-analyze: dépôts PVE ; full-upgrade) et raspbian/
  (update-analyze: espace disque ; full-upgrade)
- execute résout les actions APT par profil OS (resolveTemplate) → proxmox/
  raspbian si dispo, sinon fallback apt/ (non-régression debian/ubuntu vérifiée)
- machine_probe (lecture seule) : template + parseProbe/proposeCorrections (TDD)
  → propose os_family/machine_kind/virtualization, persiste machine_hardware,
  n'applique jamais auto ; branche execute + allowlist route
- apt_proxy_persistent : ActionType + template idempotent (/etc/apt/apt.conf.d/
  01proxy, backup) + TemplateVars.aptProxyUrl + allowlist route

tsc 0 · 95 tests · build OK · résolution OS vérifiée.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 07:14:43 +02:00
parent b5ec14dcd8
commit bafb085995
13 changed files with 490 additions and 15 deletions
+41 -15
View File
@@ -6,7 +6,7 @@ import { join } from "node:path";
import { db, schema } from "../db/client.js";
import { env } from "../env.js";
import { getMachineRow, getCreds } from "./machines.js";
import { renderTemplate } from "../templates/render.js";
import { renderTemplate, resolveTemplate } from "../templates/render.js";
import { reduceAptLines } from "../templates/aptReduce.js";
import { runScriptSudo } from "../ssh/client.js";
import { parseRebootRequired, buildAptExecutionResult } from "./aptParse.js";
@@ -18,16 +18,16 @@ import { outputHub } from "../ws/outputHub.js";
import { upsertMachineState, recordEvent } from "./machineState.js";
import type { ActionType, AptExecutionResult, ExecutionResult, ExecutionStatus } from "@shared/types.js";
const TEMPLATE_FOR: Partial<Record<ActionType, string>> = {
apt_full_upgrade: "apt/full-upgrade.sh.tpl",
apt_upgrade: "apt/upgrade.sh.tpl",
apt_autoremove: "apt/autoremove.sh.tpl",
apt_clean: "apt/clean.sh.tpl",
reboot: "apt/reboot.sh.tpl",
reboot_verified: "apt/reboot.sh.tpl",
// SJ-4 Docker (passif)
docker_scan: "docker/scan-compose.sh.tpl",
docker_inspect_current: "docker/inspect-compose.sh.tpl",
// Actions APT/système résolues par profil OS (resolveTemplate → proxmox/raspbian si dispo,
// sinon fallback apt/). La valeur est le basename d'action (sans dossier ni extension).
const APT_ACTION_FILE: Partial<Record<ActionType, string>> = {
apt_full_upgrade: "full-upgrade",
apt_upgrade: "upgrade",
apt_autoremove: "autoremove",
apt_clean: "clean",
reboot: "reboot",
reboot_verified: "reboot",
apt_proxy_persistent: "apt-proxy-persistent",
};
export interface RunActionOpts {
@@ -333,16 +333,42 @@ export async function runAction(
}
}
// --- SJ-7 : sonde machine (lecture seule) déléguée au service dédié ---
if (action === "machine_probe") {
const { runProbe } = await import("./machineProbe.js");
try {
const o = await runProbe(machineId, () => {});
const important = [
`machine_probe : os=${o.probe.osId ?? "?"} ${o.probe.osVersion ?? ""} arch=${o.probe.arch ?? "?"} virt=${o.probe.virt ?? "?"}`,
`proposition : os_family=${o.proposal.osFamily} machine_kind=${o.proposal.machineKind} virtualization=${o.proposal.virtualization}`,
...(o.changes.length ? ["corrections proposées (non appliquées) :", ...o.changes.map((c) => ` ${c}`)] : ["aucune correction proposée"]),
];
outputHub.publish(machineId, `\n===SU:DONE status=ok===\n`);
return archiveExecution({ machineId, machineName: m.name, executionId, action, startedAt, status: "ok", raw: o.raw, importantLines: important });
} catch (err) {
return archiveExecution({ machineId, machineName: m.name, executionId, action, startedAt, status: "error", raw: "", importantLines: [`[ERREUR] ${(err as Error).message}`] });
}
}
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
const rel = TEMPLATE_FOR[action];
if (!rel) throw new Error("Action sans template: " + action);
// Templates Docker par-stack (inspect) : injecter stackDir ; ignoré par les templates APT.
// Résolution du template : Docker inspect = chemin direct ; sinon résolution par profil OS.
let rel: string;
if (action === "docker_inspect_current") {
rel = "docker/inspect-compose.sh.tpl";
} else {
const file = APT_ACTION_FILE[action];
if (!file) throw new Error("Action sans template: " + action);
rel = resolveTemplate(file, m.osFamily);
}
// Docker inspect par-stack : injecter stackDir ; ignoré par les templates APT.
let stackDir: string | null = null;
if (opts?.stackId) {
const st = db.select().from(schema.dockerComposeStacks).where(eq(schema.dockerComposeStacks.id, opts.stackId)).get();
stackDir = st?.workingDir ?? null;
}
const script = renderTemplate(rel, { aptProxy: proxy, stackDir });
// Proxy persistant : l'URL est passée comme variable de template (jamais un secret).
const aptProxyUrl = action === "apt_proxy_persistent" ? m.aptProxyUrl : null;
const script = renderTemplate(rel, { aptProxy: proxy, stackDir, aptProxyUrl });
const inactivity = action === "reboot" ? 0 : 600000;