Files
gilles 1530409d3b feat(post-install): identity_network reboote et rebascule l'IP en BDD (tâche 4)
- updateMachine accepte name/hostname/port (correction d'identité réseau)
- rebootAndRebind : reboot sur l'ancienne connexion → attente du retour sur la
  NOUVELLE IP (verifyReboot avec host cible) → maj BDD (hostname + nom) si OK.
  Sécurité : BDD inchangée si la machine ne revient pas (récupération console/backups)
- execute post_install : si identity_network + reboot coché + succès, déclenche
  rebootAndRebind et joint le RebootResult ; statut error si reconnexion échoue

tsc 0 · 113 tests · build OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:12:39 +02:00

185 lines
6.8 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 { AptProxyMode, MachineKind, 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?: AptProxyMode;
aptProxyUrl?: string | null;
osFamily?: OsFamily; // choix manuel ; sinon auto-détecté via os-release
machineKind?: MachineKind;
}
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 AptProxyMode,
aptProxyUrl: m.aptProxyUrl,
status: m.status as MachineView["status"],
lastCheckedAt: m.lastCheckedAt,
machineKind: (m.machineKind as MachineKind | null) ?? "unknown",
virtualization: m.virtualization,
};
}
export interface UpdateMachineInput {
name?: string;
hostname?: string;
port?: number;
osFamily?: OsFamily;
machineKind?: MachineKind;
virtualization?: string | null;
aptProxyMode?: AptProxyMode;
aptProxyUrl?: string | null;
}
/** Met à jour les champs de profil/proxy/identité d'une machine (jamais les secrets). */
export function updateMachine(id: string, input: UpdateMachineInput): MachineView {
const row = getMachineRow(id);
if (!row) throw new Error("Machine introuvable");
const patch: Partial<MachineRow> = { updatedAt: new Date().toISOString() };
if (input.name !== undefined) patch.name = input.name;
if (input.hostname !== undefined) patch.hostname = input.hostname;
if (input.port !== undefined) patch.port = input.port;
if (input.osFamily !== undefined) patch.osFamily = input.osFamily;
if (input.machineKind !== undefined) patch.machineKind = input.machineKind;
if (input.virtualization !== undefined) patch.virtualization = input.virtualization;
if (input.aptProxyMode !== undefined) patch.aptProxyMode = input.aptProxyMode;
if (input.aptProxyUrl !== undefined) patch.aptProxyUrl = input.aptProxyUrl;
db.update(schema.machines).set(patch).where(eq(schema.machines.id, id)).run();
return toView(getMachineRow(id)!);
}
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: input.osFamily && input.osFamily !== "unknown" ? input.osFamily : os.family, // manuel prioritaire, "unknown" => auto
osVersion: os.version || null,
osCodename: null,
arch: null,
machineKind: input.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();
}
/** Faits matériels d'une machine (machine_hardware rempli par machine_probe + colonnes machines). */
export function getMachineHardware(id: string) {
const m = getMachineRow(id);
if (!m) throw new Error("Machine introuvable");
const hw = db.select().from(schema.machineHardware).where(eq(schema.machineHardware.machineId, id)).get();
const parse = <T>(j: string | null | undefined): T[] => {
try { return j ? (JSON.parse(j) as T[]) : []; } catch { return []; }
};
return {
osFamily: m.osFamily,
osVersion: m.osVersion,
arch: m.arch,
machineKind: m.machineKind,
virtualization: m.virtualization,
gpus: parse<string>(hw?.gpusJson),
network: parse<{ iface: string; addr: string }>(hw?.networkJson),
probed: !!hw,
};
}
/** Applique un proxy APT à toutes les machines. Renvoie le nombre de machines modifiées. */
export function applyProxyToAllMachines(mode: AptProxyMode, url: string | null): number {
const res = db
.update(schema.machines)
.set({ aptProxyMode: mode, aptProxyUrl: url, updatedAt: new Date().toISOString() })
.run();
return res.changes;
}