feat(os): profils Proxmox/RPi + machine_probe + proxy persistent (tâche 2 SJ-7)
- templates proxmox/ (update-analyze: dépôts PVE ; full-upgrade) et raspbian/ (update-analyze: espace disque ; full-upgrade) - execute résout les actions APT par profil OS (resolveTemplate) → proxmox/ raspbian si dispo, sinon fallback apt/ (non-régression debian/ubuntu vérifiée) - machine_probe (lecture seule) : template + parseProbe/proposeCorrections (TDD) → propose os_family/machine_kind/virtualization, persiste machine_hardware, n'applique jamais auto ; branche execute + allowlist route - apt_proxy_persistent : ActionType + template idempotent (/etc/apt/apt.conf.d/ 01proxy, backup) + TemplateVars.aptProxyUrl + allowlist route tsc 0 · 95 tests · build OK · résolution OS vérifiée. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
# Tâche 2 — SJ-7 : Profils OS Proxmox/RPi + machine_probe + proxy persistent
|
||||
|
||||
> Statut : **implémenté** (2026-06-06). tsc 0 · 95 tests · build OK. Résolution OS vérifiée.
|
||||
> Réf. design : `docs/design/tache2/60-profils-os-machine.md`, `80-sous-jalons.md` SJ-7.
|
||||
|
||||
## Périmètre livré (additif, fallback base préservé)
|
||||
|
||||
- **Templates OS-spécifiques** :
|
||||
- `templates/proxmox/update-analyze.sh.tpl` (détection dépôts PVE enterprise/no-subscription)
|
||||
+ `full-upgrade.sh.tpl` (dist-upgrade kernel/proxmox-ve/Ceph).
|
||||
- `templates/raspbian/update-analyze.sh.tpl` (contrôle espace disque carte SD)
|
||||
+ `full-upgrade.sh.tpl` (`apt full-upgrade`). `rpi-update` volontairement non utilisé.
|
||||
- **Résolution par profil OS dans `execute.ts`** : les actions APT passent par
|
||||
`resolveTemplate(file, osFamily)` → `proxmox/`/`raspbian/` si dispo, sinon `apt/`.
|
||||
Vérifié : proxmox/raspbian pris ; debian/ubuntu → fallback `apt/` (non-régression jalon 1).
|
||||
`refresh.ts` résolvait déjà `update-analyze`.
|
||||
- **`machine_probe`** (action lecture seule) :
|
||||
- `templates/apt/machine-probe.sh.tpl` (os-release, arch, systemd-detect-virt, /etc/pve,
|
||||
/proc/cpuinfo RPi, lspci GPU, ip addr).
|
||||
- `machineProbe.ts` : `parseProbe` + `proposeCorrections` (TDD, 4 cas : Proxmox/RPi/VM KVM)
|
||||
→ propose `os_family`/`machine_kind`/`virtualization`. `runProbe` persiste les faits
|
||||
matériels (`machine_hardware` gpus/network) et renvoie un diff **jamais appliqué auto**.
|
||||
- Branche `execute` (archiveExecution) + allowlist route.
|
||||
- **Proxy APT persistant** (`apt_proxy_persistent`) :
|
||||
- ActionType ajouté ; `templates/apt/apt-proxy-persistent.sh.tpl` écrit
|
||||
`/etc/apt/apt.conf.d/01proxy` (idempotent, sauvegarde horodatée de l'existant).
|
||||
- `TemplateVars.aptProxyUrl` ; rendu avec `m.aptProxyUrl` ; allowlist route.
|
||||
|
||||
## Sécurité / invariants
|
||||
|
||||
- `machine_probe` ne modifie rien ; les corrections OS/kind sont **proposées**, l'opérateur
|
||||
garde le dernier mot (pas d'application auto).
|
||||
- Proxy persistant = action explicite idempotente avec backup ; l'URL n'est pas un secret.
|
||||
- Aucun secret dans les templates ; fallback `base` garantit la non-régression Debian/Ubuntu.
|
||||
|
||||
## Reste tâche 2
|
||||
|
||||
SJ-8 / SJ-9 (post-install : bootstrap/identité, paquets de base/Docker officiel/partages/VM tools).
|
||||
UI : bouton « Sonder » + affichage des propositions, sélecteur de proxy persistant = tâche 3.
|
||||
@@ -16,6 +16,9 @@ const ALLOWED_ACTIONS: ActionType[] = [
|
||||
"docker_scan",
|
||||
"docker_inspect_current",
|
||||
"docker_pull_check",
|
||||
// SJ-7 : sonde (lecture seule) + proxy APT persistant (action explicite idempotente).
|
||||
"machine_probe",
|
||||
"apt_proxy_persistent",
|
||||
];
|
||||
// Actions Docker ciblant un stack précis : stackId obligatoire.
|
||||
const NEED_STACK: ActionType[] = ["docker_inspect_current", "docker_pull_check"];
|
||||
|
||||
+41
-15
@@ -6,7 +6,7 @@ import { join } from "node:path";
|
||||
import { db, schema } from "../db/client.js";
|
||||
import { env } from "../env.js";
|
||||
import { getMachineRow, getCreds } from "./machines.js";
|
||||
import { renderTemplate } from "../templates/render.js";
|
||||
import { renderTemplate, resolveTemplate } from "../templates/render.js";
|
||||
import { reduceAptLines } from "../templates/aptReduce.js";
|
||||
import { runScriptSudo } from "../ssh/client.js";
|
||||
import { parseRebootRequired, buildAptExecutionResult } from "./aptParse.js";
|
||||
@@ -18,16 +18,16 @@ import { outputHub } from "../ws/outputHub.js";
|
||||
import { upsertMachineState, recordEvent } from "./machineState.js";
|
||||
import type { ActionType, AptExecutionResult, ExecutionResult, ExecutionStatus } from "@shared/types.js";
|
||||
|
||||
const TEMPLATE_FOR: Partial<Record<ActionType, string>> = {
|
||||
apt_full_upgrade: "apt/full-upgrade.sh.tpl",
|
||||
apt_upgrade: "apt/upgrade.sh.tpl",
|
||||
apt_autoremove: "apt/autoremove.sh.tpl",
|
||||
apt_clean: "apt/clean.sh.tpl",
|
||||
reboot: "apt/reboot.sh.tpl",
|
||||
reboot_verified: "apt/reboot.sh.tpl",
|
||||
// SJ-4 Docker (passif)
|
||||
docker_scan: "docker/scan-compose.sh.tpl",
|
||||
docker_inspect_current: "docker/inspect-compose.sh.tpl",
|
||||
// Actions APT/système résolues par profil OS (resolveTemplate → proxmox/raspbian si dispo,
|
||||
// sinon fallback apt/). La valeur est le basename d'action (sans dossier ni extension).
|
||||
const APT_ACTION_FILE: Partial<Record<ActionType, string>> = {
|
||||
apt_full_upgrade: "full-upgrade",
|
||||
apt_upgrade: "upgrade",
|
||||
apt_autoremove: "autoremove",
|
||||
apt_clean: "clean",
|
||||
reboot: "reboot",
|
||||
reboot_verified: "reboot",
|
||||
apt_proxy_persistent: "apt-proxy-persistent",
|
||||
};
|
||||
|
||||
export interface RunActionOpts {
|
||||
@@ -333,16 +333,42 @@ export async function runAction(
|
||||
}
|
||||
}
|
||||
|
||||
// --- SJ-7 : sonde machine (lecture seule) déléguée au service dédié ---
|
||||
if (action === "machine_probe") {
|
||||
const { runProbe } = await import("./machineProbe.js");
|
||||
try {
|
||||
const o = await runProbe(machineId, () => {});
|
||||
const important = [
|
||||
`machine_probe : os=${o.probe.osId ?? "?"} ${o.probe.osVersion ?? ""} arch=${o.probe.arch ?? "?"} virt=${o.probe.virt ?? "?"}`,
|
||||
`proposition : os_family=${o.proposal.osFamily} machine_kind=${o.proposal.machineKind} virtualization=${o.proposal.virtualization}`,
|
||||
...(o.changes.length ? ["corrections proposées (non appliquées) :", ...o.changes.map((c) => ` ${c}`)] : ["aucune correction proposée"]),
|
||||
];
|
||||
outputHub.publish(machineId, `\n===SU:DONE status=ok===\n`);
|
||||
return archiveExecution({ machineId, machineName: m.name, executionId, action, startedAt, status: "ok", raw: o.raw, importantLines: important });
|
||||
} catch (err) {
|
||||
return archiveExecution({ machineId, machineName: m.name, executionId, action, startedAt, status: "error", raw: "", importantLines: [`[ERREUR] ${(err as Error).message}`] });
|
||||
}
|
||||
}
|
||||
|
||||
const proxy = m.aptProxyMode === "runtime" ? m.aptProxyUrl : null;
|
||||
const rel = TEMPLATE_FOR[action];
|
||||
if (!rel) throw new Error("Action sans template: " + action);
|
||||
// Templates Docker par-stack (inspect) : injecter stackDir ; ignoré par les templates APT.
|
||||
// Résolution du template : Docker inspect = chemin direct ; sinon résolution par profil OS.
|
||||
let rel: string;
|
||||
if (action === "docker_inspect_current") {
|
||||
rel = "docker/inspect-compose.sh.tpl";
|
||||
} else {
|
||||
const file = APT_ACTION_FILE[action];
|
||||
if (!file) throw new Error("Action sans template: " + action);
|
||||
rel = resolveTemplate(file, m.osFamily);
|
||||
}
|
||||
// Docker inspect par-stack : injecter stackDir ; ignoré par les templates APT.
|
||||
let stackDir: string | null = null;
|
||||
if (opts?.stackId) {
|
||||
const st = db.select().from(schema.dockerComposeStacks).where(eq(schema.dockerComposeStacks.id, opts.stackId)).get();
|
||||
stackDir = st?.workingDir ?? null;
|
||||
}
|
||||
const script = renderTemplate(rel, { aptProxy: proxy, stackDir });
|
||||
// Proxy persistant : l'URL est passée comme variable de template (jamais un secret).
|
||||
const aptProxyUrl = action === "apt_proxy_persistent" ? m.aptProxyUrl : null;
|
||||
const script = renderTemplate(rel, { aptProxy: proxy, stackDir, aptProxyUrl });
|
||||
|
||||
const inactivity = action === "reboot" ? 0 : 600000;
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseProbe, proposeCorrections } from "./machineProbe.js";
|
||||
|
||||
const PROXMOX = [
|
||||
"===SU:PROBE_OS===",
|
||||
'PRETTY_NAME="Debian GNU/Linux 12 (bookworm)"',
|
||||
"ID=debian",
|
||||
'VERSION_ID="12"',
|
||||
"VERSION_CODENAME=bookworm",
|
||||
"===SU:PROBE_ARCH===",
|
||||
"x86_64",
|
||||
"amd64",
|
||||
"===SU:PROBE_VIRT===",
|
||||
"none",
|
||||
"===SU:PROBE_PROXMOX===",
|
||||
"PROXMOX=1",
|
||||
"===SU:PROBE_RPI===",
|
||||
"RPI=0",
|
||||
"===SU:PROBE_GPU===",
|
||||
"01:00.0 VGA compatible controller: Matrox MGA G200eW",
|
||||
"===SU:PROBE_NET===",
|
||||
"vmbr0 10.0.3.202/24",
|
||||
"===SU:EXIT=0===",
|
||||
].join("\n");
|
||||
|
||||
const RPI = [
|
||||
"===SU:PROBE_OS===",
|
||||
"ID=debian",
|
||||
"VERSION_CODENAME=bookworm",
|
||||
"===SU:PROBE_ARCH===",
|
||||
"aarch64",
|
||||
"arm64",
|
||||
"===SU:PROBE_VIRT===",
|
||||
"none",
|
||||
"===SU:PROBE_PROXMOX===",
|
||||
"PROXMOX=0",
|
||||
"===SU:PROBE_RPI===",
|
||||
"RPI=1",
|
||||
"===SU:PROBE_GPU===",
|
||||
"no-lspci",
|
||||
"===SU:PROBE_NET===",
|
||||
"eth0 192.168.1.50/24",
|
||||
"===SU:EXIT=0===",
|
||||
].join("\n");
|
||||
|
||||
const KVM_VM = [
|
||||
"===SU:PROBE_OS===",
|
||||
"ID=ubuntu",
|
||||
'VERSION_ID="24.04"',
|
||||
"VERSION_CODENAME=noble",
|
||||
"===SU:PROBE_ARCH===",
|
||||
"x86_64",
|
||||
"amd64",
|
||||
"===SU:PROBE_VIRT===",
|
||||
"kvm",
|
||||
"===SU:PROBE_PROXMOX===",
|
||||
"PROXMOX=0",
|
||||
"===SU:PROBE_RPI===",
|
||||
"RPI=0",
|
||||
"===SU:PROBE_GPU===",
|
||||
"no-lspci",
|
||||
"===SU:PROBE_NET===",
|
||||
"ens18 10.0.3.5/24",
|
||||
"===SU:EXIT=0===",
|
||||
].join("\n");
|
||||
|
||||
describe("parseProbe", () => {
|
||||
it("extrait os-release, arch, virt et drapeaux", () => {
|
||||
const p = parseProbe(PROXMOX);
|
||||
expect(p.osId).toBe("debian");
|
||||
expect(p.osVersion).toBe("12");
|
||||
expect(p.osCodename).toBe("bookworm");
|
||||
expect(p.arch).toBe("x86_64");
|
||||
expect(p.dpkgArch).toBe("amd64");
|
||||
expect(p.virt).toBe("none");
|
||||
expect(p.isProxmox).toBe(true);
|
||||
expect(p.isRpi).toBe(false);
|
||||
expect(p.gpus).toHaveLength(1);
|
||||
expect(p.net).toEqual([{ iface: "vmbr0", addr: "10.0.3.202/24" }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("proposeCorrections", () => {
|
||||
it("Proxmox → os_family proxmox + machine_kind proxmox_host", () => {
|
||||
const c = proposeCorrections(parseProbe(PROXMOX));
|
||||
expect(c.osFamily).toBe("proxmox");
|
||||
expect(c.machineKind).toBe("proxmox_host");
|
||||
expect(c.virtualization).toBe("none");
|
||||
});
|
||||
|
||||
it("Raspberry Pi → raspbian + raspberry_pi", () => {
|
||||
const c = proposeCorrections(parseProbe(RPI));
|
||||
expect(c.osFamily).toBe("raspbian");
|
||||
expect(c.machineKind).toBe("raspberry_pi");
|
||||
});
|
||||
|
||||
it("VM KVM Ubuntu → ubuntu + vm + virtualization kvm", () => {
|
||||
const c = proposeCorrections(parseProbe(KVM_VM));
|
||||
expect(c.osFamily).toBe("ubuntu");
|
||||
expect(c.machineKind).toBe("vm");
|
||||
expect(c.virtualization).toBe("kvm");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,155 @@
|
||||
// server/services/machineProbe.ts
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db, schema } from "../db/client.js";
|
||||
import { getMachineRow, getCreds } from "./machines.js";
|
||||
import { renderTemplate } from "../templates/render.js";
|
||||
import { runScriptSudo } from "../ssh/client.js";
|
||||
import { outputHub } from "../ws/outputHub.js";
|
||||
import type { OsFamily, MachineKind } from "@shared/types.js";
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Fonctions pures (testables).
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface ProbeResult {
|
||||
osId: string | null;
|
||||
osVersion: string | null;
|
||||
osCodename: string | null;
|
||||
arch: string | null;
|
||||
dpkgArch: string | null;
|
||||
virt: string | null;
|
||||
isProxmox: boolean;
|
||||
isRpi: boolean;
|
||||
gpus: string[];
|
||||
net: { iface: string; addr: string }[];
|
||||
}
|
||||
|
||||
export interface CorrectionProposal {
|
||||
osFamily: OsFamily;
|
||||
machineKind: MachineKind;
|
||||
virtualization: string;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
function osReleaseValue(block: string, key: string): string | null {
|
||||
const m = new RegExp(`^${key}=(.*)$`, "m").exec(block);
|
||||
if (!m || m[1] === undefined) return null;
|
||||
return m[1].replace(/^"(.*)"$/, "$1").trim() || null;
|
||||
}
|
||||
|
||||
export function parseProbe(raw: string): ProbeResult {
|
||||
const os = section(raw, "===SU:PROBE_OS===", "===SU:PROBE_ARCH===");
|
||||
const archBlock = section(raw, "===SU:PROBE_ARCH===", "===SU:PROBE_VIRT===").split("\n");
|
||||
const virt = section(raw, "===SU:PROBE_VIRT===", "===SU:PROBE_PROXMOX===").split("\n")[0]?.trim() || null;
|
||||
const prox = section(raw, "===SU:PROBE_PROXMOX===", "===SU:PROBE_RPI===");
|
||||
const rpi = section(raw, "===SU:PROBE_RPI===", "===SU:PROBE_GPU===");
|
||||
const gpuBlock = section(raw, "===SU:PROBE_GPU===", "===SU:PROBE_NET===");
|
||||
const netBlock = section(raw, "===SU:PROBE_NET===", "===SU:EXIT=");
|
||||
|
||||
const gpus = gpuBlock
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l && l !== "no-lspci");
|
||||
|
||||
const net: ProbeResult["net"] = [];
|
||||
for (const line of netBlock.split("\n")) {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
if (parts.length >= 2 && parts[0] && parts[1] && parts[0] !== "lo") {
|
||||
net.push({ iface: parts[0], addr: parts[1] });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
osId: osReleaseValue(os, "ID"),
|
||||
osVersion: osReleaseValue(os, "VERSION_ID"),
|
||||
osCodename: osReleaseValue(os, "VERSION_CODENAME"),
|
||||
arch: archBlock[0]?.trim() || null,
|
||||
dpkgArch: archBlock[1]?.trim() || null,
|
||||
virt,
|
||||
isProxmox: /PROXMOX=1/.test(prox),
|
||||
isRpi: /RPI=1/.test(rpi),
|
||||
gpus,
|
||||
net,
|
||||
};
|
||||
}
|
||||
|
||||
const VM_VIRTS = new Set(["kvm", "qemu", "vmware", "oracle", "microsoft", "xen", "bochs", "parallels"]);
|
||||
const LXC_VIRTS = new Set(["lxc", "lxc-libvirt", "openvz", "systemd-nspawn", "docker", "podman"]);
|
||||
|
||||
export function proposeCorrections(p: ProbeResult): CorrectionProposal {
|
||||
const virtualization = p.virt && p.virt !== "none" ? p.virt : "none";
|
||||
|
||||
let osFamily: OsFamily;
|
||||
if (p.isProxmox) osFamily = "proxmox";
|
||||
else if (p.isRpi) osFamily = "raspbian";
|
||||
else if (p.osId === "ubuntu") osFamily = "ubuntu";
|
||||
else if (p.osId === "debian" || p.osId === "raspbian") osFamily = "debian";
|
||||
else osFamily = "unknown";
|
||||
|
||||
let machineKind: MachineKind;
|
||||
if (p.isProxmox) machineKind = "proxmox_host";
|
||||
else if (p.isRpi) machineKind = "raspberry_pi";
|
||||
else if (p.virt && VM_VIRTS.has(p.virt)) machineKind = "vm";
|
||||
else if (p.virt && LXC_VIRTS.has(p.virt)) machineKind = "lxc";
|
||||
else if (p.virt === "none") machineKind = "physical";
|
||||
else machineKind = "unknown";
|
||||
|
||||
return { osFamily, machineKind, virtualization };
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Orchestration (SSH, lecture seule). Persiste les faits matériels ; ne corrige PAS
|
||||
// os_family/machine_kind automatiquement — la proposition est renvoyée pour validation.
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export interface ProbeOutcome {
|
||||
probe: ProbeResult;
|
||||
proposal: CorrectionProposal;
|
||||
raw: string;
|
||||
changes: string[]; // diff entre l'actuel et la proposition (pour l'UI)
|
||||
}
|
||||
|
||||
export async function runProbe(machineId: string, onData?: (c: string) => void): Promise<ProbeOutcome> {
|
||||
const m = getMachineRow(machineId);
|
||||
if (!m) throw new Error("Machine introuvable");
|
||||
const script = renderTemplate("apt/machine-probe.sh.tpl", {});
|
||||
const res = await runScriptSudo(getCreds(m), script, (c) => {
|
||||
onData?.(c);
|
||||
outputHub.publish(machineId, c);
|
||||
});
|
||||
const raw = res.stdout;
|
||||
const probe = parseProbe(raw);
|
||||
const proposal = proposeCorrections(probe);
|
||||
|
||||
const now = new Date().toISOString();
|
||||
db.insert(schema.machineHardware)
|
||||
.values({
|
||||
machineId,
|
||||
gpusJson: JSON.stringify(probe.gpus),
|
||||
networkJson: JSON.stringify(probe.net),
|
||||
updatedAt: now,
|
||||
})
|
||||
.onConflictDoUpdate({
|
||||
target: schema.machineHardware.machineId,
|
||||
set: { gpusJson: JSON.stringify(probe.gpus), networkJson: JSON.stringify(probe.net), updatedAt: now },
|
||||
})
|
||||
.run();
|
||||
|
||||
const changes: string[] = [];
|
||||
if (proposal.osFamily !== m.osFamily) changes.push(`os_family: ${m.osFamily} → ${proposal.osFamily}`);
|
||||
if (proposal.machineKind !== (m.machineKind ?? "unknown")) {
|
||||
changes.push(`machine_kind: ${m.machineKind ?? "—"} → ${proposal.machineKind}`);
|
||||
}
|
||||
if (proposal.virtualization !== (m.virtualization ?? "none")) {
|
||||
changes.push(`virtualization: ${m.virtualization ?? "—"} → ${proposal.virtualization}`);
|
||||
}
|
||||
|
||||
return { probe, proposal, raw, changes };
|
||||
}
|
||||
@@ -7,6 +7,7 @@ const TEMPLATES_ROOT = resolve(process.cwd(), "templates");
|
||||
|
||||
export interface TemplateVars {
|
||||
aptProxy?: string | null;
|
||||
aptProxyUrl?: string | null; // proxy persistant (apt_proxy_persistent)
|
||||
// Docker template vars
|
||||
composeRoots?: string | number | null;
|
||||
composeScanDepth?: string | number | null;
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ActionType =
|
||||
| "apt_autoremove" | "apt_clean" | "reboot_verified"
|
||||
| "docker_scan" | "docker_inspect_current" | "docker_pull_check"
|
||||
| "docker_compose_apply" | "docker_prune_images" | "docker_compose_down"
|
||||
| "apt_proxy_persistent"
|
||||
| "machine_probe" | "post_install";
|
||||
export type ExecutionStatus = "ok" | "warning" | "error";
|
||||
export type ApiClientScope = "read" | "operate" | "admin" | "debug";
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# Proxy APT persistant : écrit /etc/apt/apt.conf.d/01proxy (idempotent, sauvegarde l'existant).
|
||||
# Action explicite (écriture disque). aptProxyUrl est fourni par le backend (jamais un secret).
|
||||
export LC_ALL=C
|
||||
CONF=/etc/apt/apt.conf.d/01proxy
|
||||
echo "===SU:PROXY_BEFORE==="
|
||||
[ -f "$CONF" ] && cat "$CONF" || echo "ABSENT"
|
||||
echo "===SU:PROXY_WRITE==="
|
||||
{{#aptProxyUrl}}
|
||||
# Sauvegarde horodatée si le fichier existe déjà.
|
||||
[ -f "$CONF" ] && cp -a "$CONF" "${CONF}.bak.$(date +%Y%m%d%H%M%S)" && echo "BACKUP=1"
|
||||
printf 'Acquire::http::Proxy "%s";\nAcquire::https::Proxy "%s";\n' "{{aptProxyUrl}}" "{{aptProxyUrl}}" > "$CONF"
|
||||
CODE=$?
|
||||
echo "WROTE=$CONF"
|
||||
{{/aptProxyUrl}}
|
||||
{{^aptProxyUrl}}
|
||||
echo "NO_PROXY_URL"
|
||||
CODE=2
|
||||
{{/aptProxyUrl}}
|
||||
echo "===SU:PROXY_AFTER==="
|
||||
cat "$CONF" 2>/dev/null || echo "ABSENT"
|
||||
echo "===SU:EXIT=${CODE}==="
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/bin/sh
|
||||
# Sonde lecture seule : OS, arch, virtualisation, Proxmox/RPi, GPU, réseau.
|
||||
# Aucune écriture. Le backend propose des corrections (jamais appliquées sans validation).
|
||||
export LC_ALL=C
|
||||
echo "===SU:PROBE_OS==="
|
||||
cat /etc/os-release 2>/dev/null
|
||||
echo "===SU:PROBE_ARCH==="
|
||||
uname -m
|
||||
dpkg --print-architecture 2>/dev/null
|
||||
echo "===SU:PROBE_VIRT==="
|
||||
systemd-detect-virt 2>/dev/null || echo "none"
|
||||
echo "===SU:PROBE_PROXMOX==="
|
||||
[ -d /etc/pve ] && echo "PROXMOX=1" || echo "PROXMOX=0"
|
||||
echo "===SU:PROBE_RPI==="
|
||||
grep -qi raspberry /proc/cpuinfo 2>/dev/null && echo "RPI=1" || echo "RPI=0"
|
||||
echo "===SU:PROBE_GPU==="
|
||||
command -v lspci >/dev/null 2>&1 && lspci 2>/dev/null | grep -Ei 'vga|3d|display' || echo "no-lspci"
|
||||
echo "===SU:PROBE_NET==="
|
||||
ip -o -4 addr show 2>/dev/null | awk '{print $2, $4}'
|
||||
echo "===SU:EXIT=0==="
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Proxmox VE : dist-upgrade (kernel PVE, proxmox-ve, Ceph). Capture diff dpkg.
|
||||
export LC_ALL=C
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
|
||||
{{/aptProxy}}
|
||||
echo "===SU:DPKG_BEFORE==="
|
||||
dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
|
||||
echo "===SU:APT_FULLUPGRADE==="
|
||||
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold dist-upgrade 2>&1
|
||||
CODE=$?
|
||||
echo "===SU:DPKG_AFTER==="
|
||||
dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
|
||||
echo "===SU:REBOOT==="
|
||||
if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
|
||||
echo "===SU:EXIT=${CODE}==="
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/bin/sh
|
||||
# Proxmox VE : refresh index + simulations + held + reboot-check + état des dépôts PVE.
|
||||
# Non destructif. Exécuté entier sous sudo par la couche SSH.
|
||||
export LC_ALL=C
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
|
||||
{{/aptProxy}}
|
||||
|
||||
echo "===SU:PVE_REPOS==="
|
||||
# Détecte le dépôt entreprise actif sans abonnement (cause classique d'échec apt update).
|
||||
grep -RhsE '^[^#]*deb .*enterprise\.proxmox\.com' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null \
|
||||
| sed 's/^/ENTERPRISE_REPO=/' || true
|
||||
grep -RhsE '^[^#]*deb .*download\.proxmox\.com.*pve-no-subscription' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null \
|
||||
| sed 's/^/NOSUB_REPO=/' || true
|
||||
|
||||
echo "===SU:APT_UPDATE==="
|
||||
apt-get update -qq 2>&1
|
||||
UPD=$?
|
||||
|
||||
echo "===SU:APT_SIM_UPGRADE==="
|
||||
apt-get -s -y upgrade 2>&1
|
||||
|
||||
echo "===SU:APT_SIM_DISTUPGRADE==="
|
||||
apt-get -s -y dist-upgrade 2>&1
|
||||
|
||||
echo "===SU:APT_HELD==="
|
||||
apt-mark showhold 2>/dev/null
|
||||
|
||||
echo "===SU:REBOOT==="
|
||||
if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then
|
||||
echo "REBOOT_REQUIRED=1"
|
||||
[ -f /var/run/reboot-required.pkgs ] && sed 's/^/PKG=/' /var/run/reboot-required.pkgs
|
||||
else
|
||||
echo "REBOOT_REQUIRED=0"
|
||||
fi
|
||||
|
||||
echo "===SU:EXIT=${UPD}==="
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# Raspberry Pi OS : full-upgrade (apt) après contrôle d'espace disque. Capture diff dpkg.
|
||||
export LC_ALL=C
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
|
||||
{{/aptProxy}}
|
||||
echo "===SU:DISK==="
|
||||
df -Pk / 2>/dev/null | awk 'NR==2{print "ROOT_AVAIL_KB="$4"\nROOT_USE_PCT="$5}'
|
||||
echo "===SU:DPKG_BEFORE==="
|
||||
dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
|
||||
echo "===SU:APT_FULLUPGRADE==="
|
||||
apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold full-upgrade 2>&1
|
||||
CODE=$?
|
||||
echo "===SU:DPKG_AFTER==="
|
||||
dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\n' 2>/dev/null
|
||||
echo "===SU:REBOOT==="
|
||||
if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then echo "REBOOT_REQUIRED=1"; else echo "REBOOT_REQUIRED=0"; fi
|
||||
echo "===SU:EXIT=${CODE}==="
|
||||
@@ -0,0 +1,34 @@
|
||||
#!/bin/sh
|
||||
# Raspberry Pi OS : refresh + simulations + held + reboot-check + espace disque (carte SD).
|
||||
# Non destructif. rpi-update volontairement NON utilisé (risqué).
|
||||
export LC_ALL=C
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
{{#aptProxy}}export http_proxy="{{aptProxy}}"; export https_proxy="{{aptProxy}}"
|
||||
{{/aptProxy}}
|
||||
|
||||
echo "===SU:DISK==="
|
||||
# Espace libre sur / en Ko (carte SD souvent petite) → le backend peut avertir avant upgrade.
|
||||
df -Pk / 2>/dev/null | awk 'NR==2{print "ROOT_AVAIL_KB="$4"\nROOT_USE_PCT="$5}'
|
||||
|
||||
echo "===SU:APT_UPDATE==="
|
||||
apt-get update -qq 2>&1
|
||||
UPD=$?
|
||||
|
||||
echo "===SU:APT_SIM_UPGRADE==="
|
||||
apt-get -s -y upgrade 2>&1
|
||||
|
||||
echo "===SU:APT_SIM_DISTUPGRADE==="
|
||||
apt-get -s -y dist-upgrade 2>&1
|
||||
|
||||
echo "===SU:APT_HELD==="
|
||||
apt-mark showhold 2>/dev/null
|
||||
|
||||
echo "===SU:REBOOT==="
|
||||
if [ -f /run/reboot-required ] || [ -f /var/run/reboot-required ]; then
|
||||
echo "REBOOT_REQUIRED=1"
|
||||
[ -f /var/run/reboot-required.pkgs ] && sed 's/^/PKG=/' /var/run/reboot-required.pkgs
|
||||
else
|
||||
echo "REBOOT_REQUIRED=0"
|
||||
fi
|
||||
|
||||
echo "===SU:EXIT=${UPD}==="
|
||||
Reference in New Issue
Block a user