d1b0290e3b
- template repositories (deb lines + deb822), non destructif - analyzeRepositories (TDD) : composants, repos, détection Proxmox enterprise/no-subscription, warnings (pve_enterprise_without_subscription, pve_repo_missing) + notes Debian/Ubuntu composants manquants - route POST /machines/:id/apt-repositories ; api analyzeRepositories - popup config : bloc « Dépôts APT » (composants + warnings + notes) Analyse uniquement (modification = action validée séparée, future). tsc 0 · 113 tests · build OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
81 lines
3.4 KiB
TypeScript
81 lines
3.4 KiB
TypeScript
// server/services/aptRepositories.ts
|
|
import { getMachineRow, getCreds } from "./machines.js";
|
|
import { renderTemplate } from "../templates/render.js";
|
|
import { runScriptSudo } from "../ssh/client.js";
|
|
import type { AptRepositoriesAnalysis, OsFamily } from "@shared/types.js";
|
|
|
|
function section(raw: string, start: string, end?: string): string {
|
|
const i = raw.indexOf(start);
|
|
if (i < 0) return "";
|
|
const from = i + start.length;
|
|
const j = end ? raw.indexOf(end, from) : -1;
|
|
return raw.slice(from, j < 0 ? undefined : j).trim();
|
|
}
|
|
|
|
interface Repo {
|
|
uri: string;
|
|
suite: string;
|
|
components: string[];
|
|
}
|
|
|
|
/** Parse les lignes `deb [opts] URI suite comp...` (format une-ligne). */
|
|
function parseDebLines(block: string): Repo[] {
|
|
const repos: Repo[] = [];
|
|
for (const line of block.split("\n")) {
|
|
const t = line.trim();
|
|
if (!t.startsWith("deb ") && !t.startsWith("deb\t")) continue;
|
|
// retire le mot-clé deb et les options [arch=...]
|
|
const rest = t.replace(/^deb\s+/, "").replace(/^\[[^\]]*\]\s*/, "");
|
|
const parts = rest.split(/\s+/).filter(Boolean);
|
|
if (parts.length < 2) continue;
|
|
const [uri, suite, ...components] = parts;
|
|
repos.push({ uri: uri!, suite: suite!, components });
|
|
}
|
|
return repos;
|
|
}
|
|
|
|
export function analyzeRepositories(osFamily: OsFamily, raw: string): AptRepositoriesAnalysis {
|
|
const repos = parseDebLines(section(raw, "===SU:REPO_DEB===", "===SU:REPO_DEB822==="));
|
|
const components = [...new Set(repos.flatMap((r) => r.components))].sort();
|
|
const warnings: AptRepositoriesAnalysis["warnings"] = [];
|
|
const notes: string[] = [];
|
|
|
|
if (osFamily === "proxmox") {
|
|
const enterprise = repos.some((r) => /enterprise\.proxmox\.com/.test(r.uri));
|
|
const noSubscription = repos.some((r) => /download\.proxmox\.com/.test(r.uri) && r.components.includes("pve-no-subscription"));
|
|
if (enterprise && !noSubscription) {
|
|
warnings.push({
|
|
kind: "pve_enterprise_without_subscription",
|
|
message: "Dépôt PVE entreprise actif sans dépôt no-subscription : `apt update` échouera sans abonnement.",
|
|
});
|
|
}
|
|
if (!enterprise && !noSubscription) {
|
|
warnings.push({ kind: "pve_repo_missing", message: "Aucun dépôt PVE détecté (ni enterprise ni no-subscription)." });
|
|
}
|
|
return { osFamily, components, repos, proxmox: { enterprise, noSubscription }, warnings, notes };
|
|
}
|
|
|
|
if (osFamily === "debian") {
|
|
for (const comp of ["contrib", "non-free", "non-free-firmware"]) {
|
|
if (!components.includes(comp)) notes.push(`Composant « ${comp} » absent (requis pour firmware/drivers propriétaires).`);
|
|
}
|
|
} else if (osFamily === "ubuntu") {
|
|
for (const comp of ["universe", "restricted", "multiverse"]) {
|
|
if (!components.includes(comp)) notes.push(`Composant « ${comp} » absent (drivers/paquets supplémentaires indisponibles).`);
|
|
}
|
|
}
|
|
|
|
if (repos.length === 0) warnings.push({ kind: "no_sources", message: "Aucune source APT détectée." });
|
|
|
|
return { osFamily, components, repos, warnings, notes };
|
|
}
|
|
|
|
/** Analyse les dépôts APT d'une machine via SSH (lecture seule). */
|
|
export async function analyzeMachineRepositories(machineId: string): Promise<AptRepositoriesAnalysis> {
|
|
const m = getMachineRow(machineId);
|
|
if (!m) throw new Error("Machine introuvable");
|
|
const script = renderTemplate("apt/repositories.sh.tpl", {});
|
|
const res = await runScriptSudo(getCreds(m), script, () => {});
|
|
return analyzeRepositories(m.osFamily as OsFamily, res.stdout);
|
|
}
|