// 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 = { 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 = {}; 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 { 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 = (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(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; }