diff --git a/docs/superpowers/plans/2026-06-06-tache2-sj9-post-install-catalogue.md b/docs/superpowers/plans/2026-06-06-tache2-sj9-post-install-catalogue.md new file mode 100644 index 0000000..7601bf6 --- /dev/null +++ b/docs/superpowers/plans/2026-06-06-tache2-sj9-post-install-catalogue.md @@ -0,0 +1,36 @@ +# Tâche 2 — SJ-9 : Catalogue post-install (paquets de base, Docker officiel, partages, VM tools) + +> Statut : **implémenté** (2026-06-06). tsc 0 · 104 tests · build OK · boot OK (8 profils servis). +> Réf. design : `docs/design/tache2/30-scripts-custom.md §2/§4`, `80-sous-jalons.md` SJ-9. +> Clôt la tâche 2. Non testé en live (installe des paquets / Docker sur une vraie machine). + +## Périmètre livré + +6 profils ajoutés au registre + 4 templates. L'UI post-install (générique, SJ-8/tâche 3) +les affiche et exécute **sans modification** — manifeste → formulaire → preview → run. + +- **Mécanisme `presetVars`** : variables fixes (non-champs) injectées au rendu, surchargées + par les valeurs de formulaire. Permet des listes de paquets prédéfinies sans champ utilisateur. +- **Templates** (`templates/custom/`) : + - `install-package-groups.sh.tpl` (générique, `{{packages}}` shell-safe). + - `docker-official-debian.sh.tpl` (clé GPG keyrings + docker.list par codename + paquets + + groupe docker + dossier compose ; relogin/reboot signalés). + - `sharing.sh.tpl` (Samba/NFS/mDNS via sections Mustache selon cases cochées). + - `vm-guest-tools.sh.tpl` (`qemu-guest-agent` ou `open-vm-tools`). +- **Manifestes** : `base_tools`, `network_tools`, `dev_git` (presetVars, sans champ, low) ; + `docker_official` (medium, confirmation, champs dockerUser/composeRoot/reboot) ; + `sharing` (medium, confirmation, bools Samba/NFS/mDNS) ; `vm_guest_tools` (low, select agent). +- Tous émettent `PKG_INSTALLED=` / `SERVICE_ENABLED=` / `ERR=` → `buildPostInstallResult` + (SJ-8) les parse sans changement. + +## Tests + +3 cas ajoutés : `base_tools` injecte bien sa liste fixe (preset), `sharing` ne rend que les +paquets cochés (sections Mustache), `docker_official` exige confirmation. + +## Bilan tâche 2 + +APT (SJ-0→3) · Docker scan/pull-check/apply-prune-down (SJ-4→6) · profils OS Proxmox/RPi + +sonde + proxy persistant (SJ-7) · post-install moteur+bootstrap+identité (SJ-8) · catalogue +post-install (SJ-9). **Volet moteur tâche 2 complet.** Catalogue détaillé/config fine +(partages, presets réutilisables, `install_profiles`/`machine_profile_state` en base) = tâche 4. diff --git a/server/services/postInstall.test.ts b/server/services/postInstall.test.ts index 503c4f4..a67d194 100644 --- a/server/services/postInstall.test.ts +++ b/server/services/postInstall.test.ts @@ -4,6 +4,7 @@ import { validateProfileValues, maskSecretValues, buildPostInstallResult, + previewProfile, type ProfileManifest, } from "./postInstall.js"; @@ -67,6 +68,26 @@ describe("maskSecretValues", () => { }); }); +describe("profils SJ-9 (presetVars + sections)", () => { + it("base_tools injecte la liste de paquets fixe", () => { + expect(PROFILES.base_tools).toBeTruthy(); + const script = previewProfile("base_tools", {}); + expect(script).toContain("nano"); + expect(script).toContain("htop"); + }); + + it("sharing ne rend que les paquets cochés", () => { + const script = previewProfile("sharing", { installSamba: true, installNfs: false, installMdns: true }); + expect(script).toContain("samba"); + expect(script).toContain("avahi-daemon"); + expect(script).not.toContain("nfs-kernel-server"); + }); + + it("docker_official exige une confirmation", () => { + expect(PROFILES.docker_official!.requiresConfirmation).toBe(true); + }); +}); + describe("buildPostInstallResult", () => { const raw = [ "===SU:CUSTOM_IDENTITY===", diff --git a/server/services/postInstall.ts b/server/services/postInstall.ts index 612060c..4f430b2 100644 --- a/server/services/postInstall.ts +++ b/server/services/postInstall.ts @@ -31,6 +31,7 @@ export interface ProfileManifest { requiresConfirmation: boolean; template: string; // chemin relatif sous templates/ fields: ProfileField[]; + presetVars?: Record; // variables fixes (ex. liste de paquets) } export const PROFILES: Record = { @@ -63,6 +64,73 @@ export const PROFILES: Record = { { name: "rebootAfterInstall", type: "bool", required: false, label: "Reboot après application" }, ], }, + base_tools: { + id: "base_tools", + label: "Outils de base", + description: "nano, less, bash-completion, tmux, screen, htop, iotop, ncdu, tree, rsync, unzip, zip, tar.", + risk: "low", + requiresConfirmation: false, + template: "custom/install-package-groups.sh.tpl", + fields: [], + presetVars: { packages: "nano less bash-completion tmux screen htop iotop ncdu tree rsync unzip zip tar" }, + }, + network_tools: { + id: "network_tools", + label: "Outils réseau", + description: "iproute2, iputils-ping, dnsutils, traceroute, tcpdump, nmap, mtr-tiny, lsof, netcat-openbsd.", + risk: "low", + requiresConfirmation: false, + template: "custom/install-package-groups.sh.tpl", + fields: [], + presetVars: { packages: "iproute2 iputils-ping dnsutils traceroute tcpdump nmap mtr-tiny lsof netcat-openbsd" }, + }, + dev_git: { + id: "dev_git", + label: "Dev / Git", + description: "git, curl, wget, jq, gnupg, lsb-release.", + risk: "low", + requiresConfirmation: false, + template: "custom/install-package-groups.sh.tpl", + fields: [], + presetVars: { packages: "git curl wget jq gnupg lsb-release" }, + }, + docker_official: { + id: "docker_official", + label: "Docker (dépôt officiel)", + description: "Docker Engine depuis le dépôt officiel Debian + plugin compose ; ajoute l'utilisateur au groupe docker.", + risk: "medium", + requiresConfirmation: true, + template: "custom/docker-official-debian.sh.tpl", + fields: [ + { name: "dockerUser", type: "string", required: true, label: "Utilisateur docker", defaultFrom: "sshUser" }, + { name: "composeRoot", type: "path", required: true, label: "Dossier Compose", default: "/home/gilles/docker" }, + { name: "rebootAfterInstall", type: "bool", required: false, label: "Reboot après installation" }, + ], + }, + sharing: { + id: "sharing", + label: "Partage réseau", + description: "Installe Samba / NFS / mDNS selon les cases cochées (configuration détaillée renvoyée à la tâche 4).", + risk: "medium", + requiresConfirmation: true, + template: "custom/sharing.sh.tpl", + fields: [ + { name: "installSamba", type: "bool", required: false, label: "Samba" }, + { name: "installNfs", type: "bool", required: false, label: "NFS" }, + { name: "installMdns", type: "bool", required: false, label: "mDNS (avahi)" }, + ], + }, + vm_guest_tools: { + id: "vm_guest_tools", + label: "Outils invité VM", + description: "Agent invité selon l'hyperviseur (qemu-guest-agent ou open-vm-tools).", + risk: "low", + requiresConfirmation: false, + template: "custom/vm-guest-tools.sh.tpl", + fields: [ + { name: "guestAgent", type: "select", required: true, label: "Agent invité", default: "qemu-guest-agent", options: ["qemu-guest-agent", "open-vm-tools"] }, + ], + }, }; // ---------------------------------------------------------------------------- @@ -156,14 +224,14 @@ function toTemplateVars(values: ProfileValues): TemplateVars { export function renderProfile(profileId: string, values: ProfileValues): string { const manifest = PROFILES[profileId]; if (!manifest) throw new Error(`Profil inconnu : ${profileId}`); - return renderTemplate(manifest.template, toTemplateVars(values)); + return renderTemplate(manifest.template, toTemplateVars({ ...manifest.presetVars, ...values })); } /** Preview du script rendu avec masquage des secrets. */ export function previewProfile(profileId: string, values: ProfileValues): string { const manifest = PROFILES[profileId]; if (!manifest) throw new Error(`Profil inconnu : ${profileId}`); - return renderTemplate(manifest.template, toTemplateVars(maskSecretValues(manifest, values))); + return renderTemplate(manifest.template, toTemplateVars({ ...manifest.presetVars, ...maskSecretValues(manifest, values) })); } // ---------------------------------------------------------------------------- diff --git a/templates/custom/docker-official-debian.sh.tpl b/templates/custom/docker-official-debian.sh.tpl new file mode 100644 index 0000000..b61ad7d --- /dev/null +++ b/templates/custom/docker-official-debian.sh.tpl @@ -0,0 +1,26 @@ +#!/bin/sh +# Docker Engine depuis le dépôt officiel Debian (docs.docker.com/engine/install/debian). +export LC_ALL=C +export DEBIAN_FRONTEND=noninteractive +echo "===SU:CUSTOM_DOCKER===" +apt-get update -qq 2>&1 +apt-get install -y ca-certificates curl 2>&1 +install -m 0755 -d /etc/apt/keyrings +curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc 2>&1 +chmod a+r /etc/apt/keyrings/docker.asc +. /etc/os-release +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list && echo "FILE_MODIFIED=/etc/apt/sources.list.d/docker.list" +apt-get update -qq 2>&1 +if apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>&1; then + for p in docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; do echo "PKG_INSTALLED=$p"; done + echo "SERVICE_ENABLED=docker" + CODE=0 +else + echo "ERR=docker_install_failed" + CODE=1 +fi +usermod -aG docker "{{dockerUser}}" 2>&1 && echo "GROUP_ADDED=docker:{{dockerUser}}" || echo "ERR=docker_group_failed" +mkdir -p "{{composeRoot}}" 2>&1 && echo "FILE_MODIFIED={{composeRoot}}" +echo "DOCKER_GROUP_RELOGIN_REQUIRED=1" +{{#rebootAfterInstall}}echo "REBOOT_REQUESTED=1"{{/rebootAfterInstall}} +echo "===SU:EXIT=${CODE}===" diff --git a/templates/custom/install-package-groups.sh.tpl b/templates/custom/install-package-groups.sh.tpl new file mode 100644 index 0000000..cd42dbd --- /dev/null +++ b/templates/custom/install-package-groups.sh.tpl @@ -0,0 +1,14 @@ +#!/bin/sh +# Installe un groupe de paquets. {{packages}} = liste shell-safe fournie par le backend. +export LC_ALL=C +export DEBIAN_FRONTEND=noninteractive +echo "===SU:CUSTOM_PKGGROUPS===" +apt-get update -qq 2>&1 +if apt-get install -y {{packages}} 2>&1; then + for p in {{packages}}; do echo "PKG_INSTALLED=$p"; done + CODE=0 +else + echo "ERR=package_install_failed" + CODE=1 +fi +echo "===SU:EXIT=${CODE}===" diff --git a/templates/custom/sharing.sh.tpl b/templates/custom/sharing.sh.tpl new file mode 100644 index 0000000..7741b96 --- /dev/null +++ b/templates/custom/sharing.sh.tpl @@ -0,0 +1,26 @@ +#!/bin/sh +# Partage réseau : installe les paquets cochés (Samba/NFS/mDNS). Config détaillée = tâche 4. +export LC_ALL=C +export DEBIAN_FRONTEND=noninteractive +echo "===SU:CUSTOM_SHARING===" +PKGS="" +{{#installSamba}}PKGS="$PKGS samba"{{/installSamba}} +{{#installNfs}}PKGS="$PKGS nfs-kernel-server"{{/installNfs}} +{{#installMdns}}PKGS="$PKGS avahi-daemon libnss-mdns"{{/installMdns}} +if [ -z "$PKGS" ]; then + echo "ERR=no_sharing_selected" + echo "===SU:EXIT=2===" + exit 2 +fi +apt-get update -qq 2>&1 +if apt-get install -y $PKGS 2>&1; then + for p in $PKGS; do echo "PKG_INSTALLED=$p"; done + {{#installSamba}}echo "SERVICE_ENABLED=smbd"{{/installSamba}} + {{#installNfs}}echo "SERVICE_ENABLED=nfs-kernel-server"{{/installNfs}} + {{#installMdns}}echo "SERVICE_ENABLED=avahi-daemon"{{/installMdns}} + CODE=0 +else + echo "ERR=sharing_install_failed" + CODE=1 +fi +echo "===SU:EXIT=${CODE}===" diff --git a/templates/custom/vm-guest-tools.sh.tpl b/templates/custom/vm-guest-tools.sh.tpl new file mode 100644 index 0000000..712454e --- /dev/null +++ b/templates/custom/vm-guest-tools.sh.tpl @@ -0,0 +1,15 @@ +#!/bin/sh +# Agent invité VM : {{guestAgent}} = qemu-guest-agent ou open-vm-tools. +export LC_ALL=C +export DEBIAN_FRONTEND=noninteractive +echo "===SU:CUSTOM_VMTOOLS===" +apt-get update -qq 2>&1 +if apt-get install -y {{guestAgent}} 2>&1; then + echo "PKG_INSTALLED={{guestAgent}}" + echo "SERVICE_ENABLED={{guestAgent}}" + CODE=0 +else + echo "ERR=vmtools_install_failed" + CODE=1 +fi +echo "===SU:EXIT=${CODE}==="