feat(post-install): moteur de profils + bootstrap + identité/réseau (tâche 2 SJ-8)

- templates custom/bootstrap-root + identity-network (sortie structurée parsable,
  sauvegardes, échec contrôlé, jamais de coupure réseau sans reconnexion)
- postInstall: registre de manifestes (champs typés + defaults/defaultFrom),
  validateProfileValues + maskSecretValues + buildPostInstallResult (TDD),
  renderProfile/previewProfile (masquage secrets), runPostInstall (SSH)
- execute: RunActionOpts.profileId/values + branche post_install (bloc postInstall)
- action_requests: post_install accepté, payload profileId/values transmis à approve
- routes: GET /profiles, POST .../preview (script masqué + validation),
  POST .../run (action_request si requiresConfirmation, sinon direct)

Champs = formulaire (pas de question SSH interactive) ; secrets jamais sérialisés ;
identity_network exige confirmation. tsc 0 · 101 tests · build OK · boot OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 08:02:32 +02:00
parent faa654c95a
commit e6f4ae470b
10 changed files with 566 additions and 3 deletions
@@ -0,0 +1,48 @@
# Tâche 2 — SJ-8 : Post-install (moteur de profils + bootstrap + identité/réseau)
> Statut : **backend implémenté** (2026-06-06). tsc 0 · 101 tests · build OK · boot OK.
> Réf. design : `docs/design/tache2/30-scripts-custom.md`, `40-contrats-json.md §4`, `80-sous-jalons.md` SJ-8.
> Non testé en live (post-install destructif : modifie sudo/réseau d'une vraie machine).
## Périmètre livré
Moteur de profils post-install non interactif : tout choix devient un **champ de
formulaire** validé côté backend ; preview avec **masquage des secrets** ; exécution
SSH + parsing `PostInstallResult` ; confirmation explicite (`action_request`) pour les
profils à risque.
## Composants
- **Templates** `templates/custom/bootstrap-root.sh.tpl` (sudo + ca-certificates + curl,
ajout groupe sudo) et `identity-network.sh.tpl` (hostname + IP statique, sauvegarde des
fichiers, jamais de coupure sans reconnexion planifiée). Sortie structurée parsable
(`PKG_INSTALLED=`, `FILE_MODIFIED=`, `OLD/NEW_ENDPOINT=`, `REBOOT_REQUESTED=1`, `ERR=`).
- **`server/services/postInstall.ts`** :
- registre `PROFILES` (manifestes : `id`, `label`, `risk`, `requiresConfirmation`,
`fields[]` avec types `string|hostname|ipv4|ipv4_cidr|ipv4_list|select|bool|int|path|secret`,
`default`/`defaultFrom`).
- `validateProfileValues` (requis + formats IPv4/CIDR/hostname) — TDD.
- `maskSecretValues` (champs `secret``********`) — TDD ; `previewProfile` masque avant rendu.
- `buildPostInstallResult` (parse → filesModified/packagesInstalled/networkChange/reboot/errors) — TDD.
- `renderProfile` / `runPostInstall` (valide puis SSH, statut ok/error).
- **`execute.ts`** : `RunActionOpts.profileId/values`, branche `post_install`
(archiveExecution + bloc `postInstall`).
- **`actionRequests.ts`** : `post_install` accepté ; payload transporte `profileId`/`values` ;
`approve` les repasse à `runAction`.
- **Routes** : `GET /api/profiles`, `POST /machines/:id/profiles/:id/preview`
(script masqué + validation), `POST /machines/:id/profiles/:id/run`
(→ `action_request` si `requiresConfirmation`, sinon exécution directe).
## Sécurité / invariants
- Aucune question interactive SSH ; échec contrôlé si décision manquante.
- Secrets jamais sérialisés : `previewProfile` masque, `variablesUsed` = non sensible only.
- `identity_network` (network_change) exige confirmation explicite via `action_request`.
- Reconnexion réseau : `OLD/NEW_ENDPOINT` + `RECONNECT_REQUIRED` remontés ; reboot via `reboot_verified` (futur).
## Reste (SJ-9 + tâche 4)
SJ-9 : profils `base_tools`, `network_tools`, `docker_official`, `sharing`, `vm_guest_tools`
(+ `install-package-groups`). Persistance `install_profiles`/`machine_profile_state`/
`script_variables_presets` et catalogue détaillé = tâche 4. UI (formulaires de profils,
preview) = tâche 3.