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>
141 lines
4.7 KiB
TypeScript
141 lines
4.7 KiB
TypeScript
// server/services/aptParse.ts
|
|
import type { AptPackage, AptSnapshotDetail, SnapshotStatus, AptChange, AptExecutionResult } from "@shared/types.js";
|
|
|
|
// Exemple de ligne:
|
|
// Inst pve-manager [8.4-1] (8.4-3 Proxmox VE:8.x [amd64])
|
|
// Inst newpkg (1.0.0 Debian:11.6/stable [all])
|
|
const INST_RE = /^Inst (\S+) (?:\[([^\]]+)\] )?\((\S+) (.+?) \[[^\]]+\]\)\s*$/;
|
|
|
|
export function parseAptSimulate(raw: string): AptPackage[] {
|
|
const out: AptPackage[] = [];
|
|
for (const line of raw.split("\n")) {
|
|
const m = INST_RE.exec(line.trimEnd());
|
|
if (!m) continue;
|
|
out.push({
|
|
name: m[1]!,
|
|
currentVersion: m[2] ?? null,
|
|
targetVersion: m[3]!,
|
|
origin: (m[4] ?? "").trim() || null,
|
|
});
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function parseRebootRequired(raw: string): boolean {
|
|
return /REBOOT_REQUIRED=1/.test(raw);
|
|
}
|
|
|
|
const REMV_RE = /^Remv (\S+)(?: \[([^\]]+)\])?/;
|
|
export function parseAptRemovals(raw: string): { name: string; currentVersion: string | null }[] {
|
|
const out: { name: string; currentVersion: string | null }[] = [];
|
|
for (const line of raw.split("\n")) {
|
|
const m = REMV_RE.exec(line.trimEnd());
|
|
if (m) out.push({ name: m[1]!, currentVersion: m[2] ?? null });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
export function parseHeld(raw: string): string[] {
|
|
return raw.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
|
|
}
|
|
|
|
export function parseRebootDetail(raw: string): { rebootRequired: boolean; pkgs: string[] } {
|
|
const rebootRequired = /REBOOT_REQUIRED=1/.test(raw);
|
|
const pkgs: string[] = [];
|
|
for (const line of raw.split("\n")) {
|
|
const m = /^PKG=(.+)$/.exec(line.trim());
|
|
if (m) pkgs.push(m[1]!.trim());
|
|
}
|
|
return { rebootRequired, pkgs };
|
|
}
|
|
|
|
export interface AptSections {
|
|
upgradeSim: string;
|
|
distUpgradeSim: string;
|
|
heldRaw: string;
|
|
rebootRaw: string;
|
|
updateFailed: boolean;
|
|
}
|
|
|
|
export function parseDpkgList(raw: string): Record<string, { version: string; arch: string }> {
|
|
const out: Record<string, { version: string; arch: string }> = {};
|
|
for (const line of raw.split("\n")) {
|
|
const parts = line.split("\t");
|
|
if (parts.length < 3) continue;
|
|
const [name, version, arch] = [parts[0]!.trim(), parts[1]!.trim(), parts[2]!.trim()];
|
|
if (!name) continue;
|
|
out[`${name}:${arch}`] = { version, arch };
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Diff dpkg réel before/after → AptExecutionResult (planned/held vides : portés par le snapshot). */
|
|
export function buildAptExecutionResult(beforeRaw: string, afterRaw: string, rebootRaw: string): AptExecutionResult {
|
|
const before = parseDpkgList(beforeRaw);
|
|
const after = parseDpkgList(afterRaw);
|
|
const applied: AptChange[] = [];
|
|
const installed: AptChange[] = [];
|
|
const removed: AptChange[] = [];
|
|
|
|
for (const key of Object.keys(after)) {
|
|
const [name] = key.split(":");
|
|
const a = after[key]!;
|
|
const b = before[key];
|
|
if (!b) {
|
|
const change: AptChange = { name: name!, arch: a.arch, fromVersion: null, toVersion: a.version, operation: "installed" };
|
|
installed.push(change); applied.push(change);
|
|
} else if (b.version !== a.version) {
|
|
applied.push({ name: name!, arch: a.arch, fromVersion: b.version, toVersion: a.version, operation: "upgraded" });
|
|
}
|
|
}
|
|
for (const key of Object.keys(before)) {
|
|
if (!after[key]) {
|
|
const [name] = key.split(":");
|
|
const b = before[key]!;
|
|
const change: AptChange = { name: name!, arch: b.arch, fromVersion: b.version, toVersion: null, operation: "removed" };
|
|
removed.push(change); applied.push(change);
|
|
}
|
|
}
|
|
|
|
return {
|
|
planned: [],
|
|
applied,
|
|
installed,
|
|
removed,
|
|
held: [],
|
|
rebootRequiredAfterRun: /REBOOT_REQUIRED=1/.test(rebootRaw),
|
|
};
|
|
}
|
|
|
|
export function buildAptSnapshotDetail(s: AptSections): AptSnapshotDetail {
|
|
const upgradePkgs = parseAptSimulate(s.upgradeSim);
|
|
const distPkgs = parseAptSimulate(s.distUpgradeSim);
|
|
const installed: AptPackage[] = distPkgs
|
|
.filter((p) => p.currentVersion === null)
|
|
.map((p) => ({ ...p, operation: "install" as const }));
|
|
const removed: AptPackage[] = parseAptRemovals(s.distUpgradeSim).map((r) => ({
|
|
name: r.name, currentVersion: r.currentVersion, targetVersion: "", origin: null, operation: "remove" as const,
|
|
}));
|
|
const held = parseHeld(s.heldRaw);
|
|
const { rebootRequired, pkgs: rebootPkgs } = parseRebootDetail(s.rebootRaw);
|
|
|
|
let status: SnapshotStatus = "ok";
|
|
if (s.updateFailed) status = "error";
|
|
else if (removed.length > 0 || held.length > 0) status = "warning";
|
|
else if (distPkgs.length > 0) status = "updates_available";
|
|
|
|
return {
|
|
enabled: true,
|
|
count: distPkgs.length,
|
|
rebootRequired,
|
|
packages: distPkgs,
|
|
status,
|
|
upgradeCount: upgradePkgs.length,
|
|
distUpgradeCount: distPkgs.length,
|
|
installed,
|
|
removed,
|
|
held,
|
|
rebootPkgs,
|
|
};
|
|
}
|