diff --git a/docs/superpowers/plans/2026-06-06-tache2-sj7-profils-os-proxy.md b/docs/superpowers/plans/2026-06-06-tache2-sj7-profils-os-proxy.md new file mode 100644 index 0000000..e57b759 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-tache2-sj7-profils-os-proxy.md @@ -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. diff --git a/server/routes/actions.ts b/server/routes/actions.ts index 33997d0..1e46db0 100644 --- a/server/routes/actions.ts +++ b/server/routes/actions.ts @@ -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"]; diff --git a/server/services/execute.ts b/server/services/execute.ts index 8745e19..9387547 100644 --- a/server/services/execute.ts +++ b/server/services/execute.ts @@ -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> = { - 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> = { + 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; diff --git a/server/services/machineProbe.test.ts b/server/services/machineProbe.test.ts new file mode 100644 index 0000000..c62ea12 --- /dev/null +++ b/server/services/machineProbe.test.ts @@ -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"); + }); +}); diff --git a/server/services/machineProbe.ts b/server/services/machineProbe.ts new file mode 100644 index 0000000..b49fc54 --- /dev/null +++ b/server/services/machineProbe.ts @@ -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 { + 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 }; +} diff --git a/server/templates/render.ts b/server/templates/render.ts index 168f4ce..499b43c 100644 --- a/server/templates/render.ts +++ b/server/templates/render.ts @@ -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; diff --git a/shared/types.ts b/shared/types.ts index bc56a48..3c53be8 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -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"; diff --git a/templates/apt/apt-proxy-persistent.sh.tpl b/templates/apt/apt-proxy-persistent.sh.tpl new file mode 100644 index 0000000..7d4d588 --- /dev/null +++ b/templates/apt/apt-proxy-persistent.sh.tpl @@ -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}===" diff --git a/templates/apt/machine-probe.sh.tpl b/templates/apt/machine-probe.sh.tpl new file mode 100644 index 0000000..7d6fd8c --- /dev/null +++ b/templates/apt/machine-probe.sh.tpl @@ -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===" diff --git a/templates/proxmox/full-upgrade.sh.tpl b/templates/proxmox/full-upgrade.sh.tpl new file mode 100644 index 0000000..de18263 --- /dev/null +++ b/templates/proxmox/full-upgrade.sh.tpl @@ -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}===" diff --git a/templates/proxmox/update-analyze.sh.tpl b/templates/proxmox/update-analyze.sh.tpl new file mode 100644 index 0000000..2bac914 --- /dev/null +++ b/templates/proxmox/update-analyze.sh.tpl @@ -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}===" diff --git a/templates/raspbian/full-upgrade.sh.tpl b/templates/raspbian/full-upgrade.sh.tpl new file mode 100644 index 0000000..ea428ee --- /dev/null +++ b/templates/raspbian/full-upgrade.sh.tpl @@ -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}===" diff --git a/templates/raspbian/update-analyze.sh.tpl b/templates/raspbian/update-analyze.sh.tpl new file mode 100644 index 0000000..325d1ae --- /dev/null +++ b/templates/raspbian/update-analyze.sh.tpl @@ -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}==="