feat: socle BDD (tâche 1.9 Phase 1-2) + moteur APT (tâche 2 SJ-0→3) + WIP capabilities/auth/Rust
Checkpoint multi-chantiers (arbre vert : tsc 0 erreur, 70 tests, build OK). - tâche 1.9 Phase 1 : schéma socle (machine_state/events/reports/raw_artifacts/ hardware/metrics + colonnes étendues) + wiring refresh/execute. Migration 0002. - tâche 1.9 Phase 2 : machine_credentials + machine_host_keys (non destructif, dual-read + backfill). Migration 0003. Fix séquence journal de migration. - tâche 2 : SJ-0 (types étendus rétro-compatibles, réducteur Docker, resolveTemplate), SJ-1 (update-analyze enrichi), SJ-2 (apply + diff dpkg + timeout inactivité SSH), SJ-3 (reboot vérifié boot_id). - WIP parallèle inclus : /api/capabilities, auth/apiTokens/apiClients, system metrics, scaffold app_rust, ajustements frontend. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+102
-7
@@ -1,7 +1,7 @@
|
||||
// server/services/execute.ts
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { mkdirSync, writeFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { db, schema } from "../db/client.js";
|
||||
import { env } from "../env.js";
|
||||
@@ -9,15 +9,22 @@ import { getMachineRow, getCreds } from "./machines.js";
|
||||
import { renderTemplate } from "../templates/render.js";
|
||||
import { reduceAptLines } from "../templates/aptReduce.js";
|
||||
import { runScriptSudo } from "../ssh/client.js";
|
||||
import { parseRebootRequired } from "./aptParse.js";
|
||||
import { parseRebootRequired, buildAptExecutionResult } from "./aptParse.js";
|
||||
import { parseBootIdBefore, verifyReboot } from "./rebootVerify.js";
|
||||
import type { RebootResult } from "@shared/types.js";
|
||||
import { extractSection } from "./refresh.js";
|
||||
import { buildReportMarkdown } from "./report.js";
|
||||
import { outputHub } from "../ws/outputHub.js";
|
||||
import type { ActionType, ExecutionResult, ExecutionStatus } from "@shared/types.js";
|
||||
import { upsertMachineState, recordEvent } from "./machineState.js";
|
||||
import type { ActionType, AptExecutionResult, ExecutionResult, ExecutionStatus } from "@shared/types.js";
|
||||
|
||||
const TEMPLATE_FOR: Record<ActionType, string> = {
|
||||
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",
|
||||
};
|
||||
|
||||
export async function runAction(machineId: string, action: ActionType): Promise<ExecutionResult> {
|
||||
@@ -31,9 +38,14 @@ export async function runAction(machineId: string, action: ActionType): Promise<
|
||||
db.insert(schema.executions).values({
|
||||
id: executionId, machineId, action, mode: "manual", startedAt, status: "running",
|
||||
}).run();
|
||||
upsertMachineState(machineId, { status: "running", runningJobId: executionId });
|
||||
|
||||
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
|
||||
const script = renderTemplate(TEMPLATE_FOR[action], { aptProxy: proxy });
|
||||
const rel = TEMPLATE_FOR[action];
|
||||
if (!rel) throw new Error("Action sans template: " + action);
|
||||
const script = renderTemplate(rel, { aptProxy: proxy });
|
||||
|
||||
const inactivity = action === "reboot" ? 0 : 600000;
|
||||
|
||||
let raw = "";
|
||||
let status: ExecutionStatus = "ok";
|
||||
@@ -41,7 +53,7 @@ export async function runAction(machineId: string, action: ActionType): Promise<
|
||||
const res = await runScriptSudo(getCreds(m), script, (c) => {
|
||||
raw += c;
|
||||
outputHub.publish(machineId, c);
|
||||
});
|
||||
}, inactivity);
|
||||
raw = res.stdout;
|
||||
if (/===SU:EXIT=\d+===/.test(raw) && !/===SU:EXIT=0===/.test(raw)) {
|
||||
status = "error";
|
||||
@@ -51,9 +63,41 @@ export async function runAction(machineId: string, action: ActionType): Promise<
|
||||
raw += `\n[ERREUR] ${(err as Error).message}\n`;
|
||||
}
|
||||
|
||||
// Vérification réseau du reboot (nouvelle action reboot_verified, jalon SJ-3).
|
||||
let rebootResult: RebootResult | undefined;
|
||||
if (action === "reboot_verified") {
|
||||
const beforeBootId = parseBootIdBefore(raw);
|
||||
outputHub.publish(machineId, "\n[reboot] attente du redémarrage...\n");
|
||||
rebootResult = await verifyReboot(getCreds(m), { beforeBootId, requestedAt: startedAt });
|
||||
if (rebootResult.status !== "ok") status = "error";
|
||||
if (rebootResult.status === "ok") {
|
||||
recordEvent({
|
||||
machineId,
|
||||
eventType: "reboot_verified",
|
||||
severity: "info",
|
||||
executionId,
|
||||
message: `Reboot vérifié en ${rebootResult.waitedSeconds}s (boot_id changé)`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const finishedAt = new Date().toISOString();
|
||||
const rebootRequired = parseRebootRequired(extractSection(raw, "===SU:REBOOT===", "===SU:EXIT") || raw);
|
||||
|
||||
// Diff dpkg réel (si le template a émis DPKG_BEFORE + DPKG_AFTER).
|
||||
let aptResult: AptExecutionResult | undefined;
|
||||
if (raw.includes("===SU:DPKG_BEFORE===") && raw.includes("===SU:DPKG_AFTER===")) {
|
||||
const afterBeforeMarker =
|
||||
raw.includes("===SU:APT_FULLUPGRADE===") ? "===SU:APT_FULLUPGRADE===" :
|
||||
raw.includes("===SU:APT_UPGRADE===") ? "===SU:APT_UPGRADE===" :
|
||||
"===SU:APT_AUTOREMOVE===";
|
||||
aptResult = buildAptExecutionResult(
|
||||
extractSection(raw, "===SU:DPKG_BEFORE===", afterBeforeMarker),
|
||||
extractSection(raw, "===SU:DPKG_AFTER===", "===SU:REBOOT==="),
|
||||
extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"),
|
||||
);
|
||||
}
|
||||
|
||||
// Archivage log brut + rapport.
|
||||
const dir = join(env.reportsDir, machineId);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
@@ -66,15 +110,66 @@ export async function runAction(machineId: string, action: ActionType): Promise<
|
||||
rebootRequiredAfterRun: rebootRequired,
|
||||
importantLogLines: reduceAptLines(raw),
|
||||
rawLogRef: rawLogPath, reportRef: reportPath,
|
||||
...(aptResult ? { apt: aptResult } : {}),
|
||||
...(rebootResult ? { reboot: rebootResult } : {}),
|
||||
};
|
||||
writeFileSync(reportPath, buildReportMarkdown(result, m.name), "utf8");
|
||||
|
||||
const reportId = randomUUID();
|
||||
const exitMatch = /===SU:EXIT=(\d+)===/.exec(raw);
|
||||
db.update(schema.executions).set({
|
||||
finishedAt, status, resultJson: JSON.stringify(result), reportPath, rawLogPath,
|
||||
finishedAt,
|
||||
status,
|
||||
schemaVersion: 1,
|
||||
resultJson: JSON.stringify(result),
|
||||
importantJson: JSON.stringify(result.importantLogLines),
|
||||
reportPath,
|
||||
rawLogPath,
|
||||
reportId,
|
||||
exitCode: exitMatch ? Number(exitMatch[1]) : null,
|
||||
errorKind: status === "error" ? "execution_failed" : null,
|
||||
errorMessage: status === "error" ? (result.importantLogLines.at(-1) ?? null) : null,
|
||||
}).where(eq(schema.executions.id, executionId)).run();
|
||||
db.update(schema.machines).set({ status: status === "error" ? "error" : "unknown" })
|
||||
.where(eq(schema.machines.id, machineId)).run();
|
||||
|
||||
db.insert(schema.reports).values({
|
||||
id: reportId,
|
||||
machineId,
|
||||
executionId,
|
||||
kind: "machine",
|
||||
title: `${m.name} — ${action}`,
|
||||
path: reportPath,
|
||||
createdAt: finishedAt,
|
||||
}).run();
|
||||
|
||||
db.insert(schema.rawArtifacts).values({
|
||||
id: randomUUID(),
|
||||
machineId,
|
||||
kind: "raw_log",
|
||||
path: rawLogPath,
|
||||
bytes: statSync(rawLogPath).size,
|
||||
createdAt: finishedAt,
|
||||
retentionPolicy: status === "error" ? "failed" : "default",
|
||||
}).run();
|
||||
|
||||
upsertMachineState(machineId, {
|
||||
status: status === "error" ? "error" : "unknown",
|
||||
runningJobId: null,
|
||||
lastErrorKind: status === "error" ? "execution_failed" : null,
|
||||
lastErrorMessage: status === "error" ? (result.importantLogLines.at(-1) ?? null) : null,
|
||||
});
|
||||
|
||||
const execSeverity: "info" | "warning" | "error" =
|
||||
status === "error" ? "error" : (status as string) === "warning" ? "warning" : "info";
|
||||
recordEvent({
|
||||
machineId,
|
||||
eventType: `action_${action}`,
|
||||
severity: execSeverity,
|
||||
executionId,
|
||||
message: `Action ${action} : ${status}`,
|
||||
});
|
||||
|
||||
outputHub.publish(machineId, `\n===SU:DONE status=${status}===\n`);
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user