58abebf687
- AddMachineModal : sélecteurs OS + Type machine ; createMachine accepte osFamily/machineKind (manuel prioritaire, "Autre/auto" → détection os-release) - section Hardware sur la tuile + panneau détail : os/type/virt/arch/gpu/réseau depuis machine_hardware (sonde) via GET /machines/:id/hardware - identité : favicon.svg (serveur + LED Gruvbox), favicon.ico, apple-touch-icon, PWA 192/512, site.webmanifest ; liens + theme-color dans index.html tsc 0 · 104 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 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 {
|
|
osFamily?: OsFamily;
|
|
machineKind?: MachineKind;
|
|
virtualization?: string | null;
|
|
aptProxyMode?: AptProxyMode;
|
|
aptProxyUrl?: string | null;
|
|
}
|
|
|
|
/** Met à jour les champs de profil/proxy 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.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;
|
|
}
|