Files
system_update/server/services/machines.ts
T
gilles 08919752e3 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>
2026-06-05 19:50:25 +02:00

124 lines
4.1 KiB
TypeScript

// server/services/machines.ts
import { randomUUID } from "node:crypto";
import { eq } from "drizzle-orm";
import { db, schema } from "../db/client.js";
import { encryptSecret, decryptSecret } from "../crypto/secrets.js";
import { env } from "../env.js";
import { runPlain, type SshCreds } from "../ssh/client.js";
import type { MachineView, OsFamily } from "@shared/types.js";
import { writeCredentials, readCredentials, resolveCreds } from "./credentials.js";
export interface CreateMachineInput {
name: string;
hostname: string;
port: number;
username: string;
password: string;
sudoPassword?: string | null;
aptProxyMode?: "direct" | "runtime";
aptProxyUrl?: string | null;
}
type MachineRow = typeof schema.machines.$inferSelect;
function toView(m: MachineRow): MachineView {
return {
id: m.id,
name: m.name,
hostname: m.hostname,
port: m.port,
osFamily: m.osFamily as OsFamily,
username: m.username,
aptProxyMode: m.aptProxyMode as "direct" | "runtime",
aptProxyUrl: m.aptProxyUrl,
status: m.status as MachineView["status"],
lastCheckedAt: m.lastCheckedAt,
};
}
export function getCreds(m: MachineRow): SshCreds {
const key = env.requireMasterKey();
const { encPassword, encSudoPassword } = resolveCreds(
{ encPassword: m.encPassword, encSudoPassword: m.encSudoPassword },
readCredentials(m.id),
);
if (!encPassword) throw new Error("Aucun secret pour cette machine");
return {
hostname: m.hostname,
port: m.port,
username: m.username,
password: decryptSecret(encPassword, key),
sudoPassword: encSudoPassword ? decryptSecret(encSudoPassword, key) : null,
};
}
export function getMachineRow(id: string): MachineRow | undefined {
return db.select().from(schema.machines).where(eq(schema.machines.id, id)).get();
}
export function listMachines(): MachineView[] {
return db.select().from(schema.machines).all().map(toView);
}
/** Parse /etc/os-release pour déduire family + version. */
export function parseOsRelease(content: string): { family: OsFamily; version: string } {
const fields: Record<string, string> = {};
for (const line of content.split("\n")) {
const m = /^([A-Z_]+)=(.*)$/.exec(line.trim());
if (m) fields[m[1]!] = m[2]!.replace(/^"|"$/g, "");
}
const id = (fields.ID ?? "").toLowerCase();
const family: OsFamily = id === "ubuntu" ? "ubuntu" : id === "debian" ? "debian" : "unknown";
return { family, version: fields.VERSION_ID ?? fields.VERSION ?? "" };
}
export async function testConnection(creds: SshCreds): Promise<{ family: OsFamily; version: string }> {
const res = await runPlain(creds, "cat /etc/os-release");
return parseOsRelease(res.stdout);
}
export async function createMachine(input: CreateMachineInput): Promise<MachineView> {
const key = env.requireMasterKey();
const creds: SshCreds = {
hostname: input.hostname,
port: input.port,
username: input.username,
password: input.password,
sudoPassword: input.sudoPassword ?? null,
};
const os = await testConnection(creds); // lève si la connexion échoue
const id = randomUUID();
const now = new Date().toISOString();
const row: MachineRow = {
id,
name: input.name,
hostname: input.hostname,
port: input.port,
osFamily: os.family,
osVersion: os.version || null,
osCodename: null,
arch: null,
machineKind: null,
virtualization: null,
hardwareProfile: null,
username: input.username,
encPassword: encryptSecret(input.password, key),
encSudoPassword: input.sudoPassword ? encryptSecret(input.sudoPassword, key) : null,
aptProxyMode: input.aptProxyMode ?? "direct",
aptProxyUrl: input.aptProxyUrl ?? null,
status: "unknown",
lastCheckedAt: null,
lastSeenAt: null,
createdAt: now,
updatedAt: now,
deletedAt: null,
};
db.insert(schema.machines).values(row).run();
writeCredentials({ machineId: id, encPassword: row.encPassword, encSudoPassword: row.encSudoPassword });
return toView(row);
}
export function deleteMachine(id: string): void {
db.delete(schema.machines).where(eq(schema.machines.id, id)).run();
}