08919752e3
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>
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
// server/services/refresh.ts
|
|
import { randomUUID } from "node:crypto";
|
|
import { eq, desc } from "drizzle-orm";
|
|
import { db, schema } from "../db/client.js";
|
|
import { getMachineRow, getCreds } from "./machines.js";
|
|
import { renderTemplate, resolveTemplate } from "../templates/render.js";
|
|
import { reduceAptLines } from "../templates/aptReduce.js";
|
|
import { runScriptSudo } from "../ssh/client.js";
|
|
import { buildAptSnapshotDetail } from "./aptParse.js";
|
|
import { outputHub } from "../ws/outputHub.js";
|
|
import { deriveAptState, upsertMachineState, recordEvent } from "./machineState.js";
|
|
import type { UpdateSnapshot, MachineStatus, AptSnapshotDetail } from "@shared/types.js";
|
|
|
|
/** Extrait la section entre deux marqueurs ===SU:X=== d'une sortie de script. */
|
|
export function extractSection(raw: string, start: string, end: string): string {
|
|
const s = raw.indexOf(start);
|
|
if (s === -1) return "";
|
|
const from = s + start.length;
|
|
const e = raw.indexOf(end, from);
|
|
return raw.slice(from, e === -1 ? undefined : e).trim();
|
|
}
|
|
|
|
export async function refreshMachine(machineId: string): Promise<UpdateSnapshot> {
|
|
const m = getMachineRow(machineId);
|
|
if (!m) throw new Error("Machine introuvable");
|
|
|
|
db.update(schema.machines).set({ status: "running" }).where(eq(schema.machines.id, machineId)).run();
|
|
outputHub.clear(machineId);
|
|
|
|
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
|
|
const script = renderTemplate(resolveTemplate("update-analyze", m.osFamily), { aptProxy: proxy });
|
|
|
|
let raw = "";
|
|
try {
|
|
const res = await runScriptSudo(getCreds(m), script, (c) => {
|
|
raw += c;
|
|
outputHub.publish(machineId, c);
|
|
});
|
|
raw = res.stdout;
|
|
} catch (err) {
|
|
db.update(schema.machines).set({ status: "error" }).where(eq(schema.machines.id, machineId)).run();
|
|
throw err;
|
|
}
|
|
|
|
const updateExit = /===SU:EXIT=(\d+)===/.exec(raw);
|
|
const detail: AptSnapshotDetail = buildAptSnapshotDetail({
|
|
upgradeSim: extractSection(raw, "===SU:APT_SIM_UPGRADE===", "===SU:APT_SIM_DISTUPGRADE==="),
|
|
distUpgradeSim: extractSection(raw, "===SU:APT_SIM_DISTUPGRADE===", "===SU:APT_HELD==="),
|
|
heldRaw: extractSection(raw, "===SU:APT_HELD===", "===SU:REBOOT==="),
|
|
rebootRaw: extractSection(raw, "===SU:REBOOT===", "===SU:EXIT"),
|
|
updateFailed: updateExit ? Number(updateExit[1]) !== 0 : false,
|
|
});
|
|
|
|
// MachineStatus n'a pas "warning" : warning => updates_available côté machine.
|
|
const status: MachineStatus =
|
|
detail.status === "error" ? "error" : detail.count > 0 || detail.status === "warning" ? "updates_available" : "ok";
|
|
const checkedAt = new Date().toISOString();
|
|
|
|
const snapshot: UpdateSnapshot = {
|
|
machineId,
|
|
hostname: m.hostname,
|
|
os: { family: m.osFamily as UpdateSnapshot["os"]["family"], version: m.osVersion ?? "" },
|
|
checkedAt,
|
|
status,
|
|
apt: detail,
|
|
schemaVersion: 1,
|
|
kind: "apt_update_analyze",
|
|
rawHints: { logImportantLines: reduceAptLines(raw) },
|
|
};
|
|
|
|
const snapshotId = randomUUID();
|
|
db.insert(schema.snapshots).values({
|
|
id: snapshotId,
|
|
machineId,
|
|
kind: "apt_update_analyze",
|
|
schemaVersion: 1,
|
|
checkedAt,
|
|
status,
|
|
payloadJson: JSON.stringify(snapshot),
|
|
importantJson: JSON.stringify(snapshot.rawHints?.logImportantLines ?? []),
|
|
}).run();
|
|
db.update(schema.machines).set({ status, lastCheckedAt: checkedAt }).where(eq(schema.machines.id, machineId)).run();
|
|
|
|
upsertMachineState(machineId, deriveAptState(snapshot));
|
|
recordEvent({
|
|
machineId,
|
|
eventType: "apt_refresh",
|
|
severity: "info",
|
|
snapshotId,
|
|
message: `Refresh APT : ${snapshot.apt.count} mise(s) à jour`,
|
|
});
|
|
|
|
return snapshot;
|
|
}
|
|
|
|
export function getLatestSnapshot(machineId: string): UpdateSnapshot | null {
|
|
const row = db
|
|
.select()
|
|
.from(schema.snapshots)
|
|
.where(eq(schema.snapshots.machineId, machineId))
|
|
.orderBy(desc(schema.snapshots.checkedAt))
|
|
.get();
|
|
return row ? (JSON.parse(row.payloadJson) as UpdateSnapshot) : null;
|
|
}
|