feat(post-install): catalogue de profils — paquets, Docker officiel, partages, VM tools (tâche 2 SJ-9)

- mécanisme presetVars (variables fixes injectées au rendu, surchargées par le formulaire)
- 6 profils : base_tools / network_tools / dev_git (listes de paquets, low),
  docker_official (dépôt officiel Debian, confirmation), sharing (Samba/NFS/mDNS, confirmation),
  vm_guest_tools (qemu/vmware)
- 4 templates custom (install-package-groups, docker-official-debian, sharing, vm-guest-tools)
  émettant PKG_INSTALLED/SERVICE_ENABLED/ERR → réutilise buildPostInstallResult
- l'UI post-install générique les expose automatiquement (manifeste → formulaire → run)

tsc 0 · 104 tests · build OK · boot OK (8 profils servis). Clôt le volet moteur tâche 2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 12:40:20 +02:00
parent 4eb0335900
commit 3b16fdd52a
7 changed files with 208 additions and 2 deletions
@@ -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.
+21
View File
@@ -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===",
+70 -2
View File
@@ -31,6 +31,7 @@ export interface ProfileManifest {
requiresConfirmation: boolean;
template: string; // chemin relatif sous templates/
fields: ProfileField[];
presetVars?: Record<string, string | number | boolean>; // variables fixes (ex. liste de paquets)
}
export const PROFILES: Record<string, ProfileManifest> = {
@@ -63,6 +64,73 @@ export const PROFILES: Record<string, ProfileManifest> = {
{ 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) }));
}
// ----------------------------------------------------------------------------
@@ -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}==="
@@ -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}==="
+26
View File
@@ -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}==="
+15
View File
@@ -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}==="