# 30 — Scripts personnalisés / post-install : modèle moteur > Axe E + livrables §4.1/§4.7. Conçu pour cocher `validation_tache2.md §8` (« scripts post-install et profils personnalisés »). **Le catalogue détaillé est renvoyé à la tâche 4** ; ce document pose le **mécanisme moteur**, les **manifestes**, les **champs dynamiques** et les **garde-fous**. --- ## 1. Principe UX et absence d'interactivité SSH - **Interdiction stricte des questions interactives au milieu d'un script SSH.** Toute question nécessaire devient un **champ de formulaire** dans la webapp avant exécution. - Les profils post-install sont **cochables** ; cocher un profil déplie ses champs obligatoires. - Chaque profil fournit un **manifeste** : `id`, `label`, `description`, `fields`, valeurs par défaut, validations, prévisualisation, `risk`, `requiresConfirmation`. - Le bouton d'exécution reste **désactivé** tant que les champs requis des profils cochés ne sont pas valides. - La webapp propose une **preview du template rendu** avant exécution (`preview_template`), avec **masquage des secrets** et signalement des changements réseau/reboot. - Les scripts s'exécutent en **mode non interactif** ; s'ils détectent une décision non fournie, ils **échouent avec une erreur structurée** au lieu de bloquer. Stockage : `install_profiles` (catalogue + `manifest_json`), `install_recipes`/`install_recipe_versions` (scripts versionnés + sha256), `machine_profile_state` (état par machine, variables **non sensibles**), `script_variables_presets` (préréglages réutilisables). Cf. `tache1.9.md §9`. --- ## 2. Profils post-install attendus (composables) | Profil | Rôle | Risque | Confirmation | |---|---|---|---| | `bootstrap_root` | Première prépa après DHCP/`su -` : installe `sudo`, `resolvconf`, `ca-certificates`, `curl` ; ajoute l'opérateur au groupe `sudo` ; vérifie `sudo`. | low | non | | `identity_network` | Hostname, domaine/search `.home`, `/etc/hosts`, IP statique dans `/etc/network/interfaces`. | network_change | **oui** | | `base_tools` | Outils de base **sans vim** : `nano`, `less`, `bash-completion`, `tmux`, `screen`, `htop`, `iotop`, `ncdu`, `tree`, `rsync`, `unzip`, `zip`, `tar`. | low | non | | `network_tools` | `iproute2`, `iputils-ping`, `dnsutils`, `traceroute`, `net-tools`(opt), `tcpdump`, `nmap`, `mtr-tiny`, `lsof`, `netcat-openbsd`. | low | non | | `dev_git` | `git`, `curl`, `wget`, `jq`, `yq`, `gnupg`, `lsb-release` ; `build-essential` optionnel. | low | non | | `sharing` | `samba`, `nfs-kernel-server`, `avahi-daemon`, `libnss-mdns`, configurables séparément. | medium | oui | | `docker_official` | Docker Engine depuis le dépôt officiel Debian : `docker-ce`, `docker-ce-cli`, `containerd.io`, `docker-buildx-plugin`, `docker-compose-plugin` ; ajout user au groupe `docker` ; dossier Compose dans le home ; reboot/reconnexion si nécessaire. | medium | oui | | `vm_guest_tools` | `qemu-guest-agent` ou `open-vm-tools` selon hyperviseur choisi. | low | non | | Optionnels (non installés par défaut) | `security_basic`, `backup_tools`, `monitoring`, `mail_notify`, `time_sync`, `storage_tools`. | variable | selon profil | > Les scripts hardware/drivers/benchmark ne sont **jamais installés par défaut** et exigent validation (voir `60-profils-os-machine.md`). --- ## 3. Champs dynamiques générés - **`identity_network`** : `newHostname`, `domain`/`search`, `interfaceName`, `staticAddress` (CIDR, ex. `10.0.x.y/22`), `gateway` (défaut `10.0.0.1`), `dnsNameservers` (défaut `10.0.0.1`, `10.0.0.10`), `reconnectHost`. - **`docker_official`** : `dockerUser` (ex. `gilles`), `dockerHomeDir`/`composeRoot` (ex. `/home/gilles/docker`), `installComposePlugin`, `rebootAfterInstall`. - **`sharing`** : choix séparé Samba/NFS/mDNS, noms de partages, chemins autorisés, utilisateurs/groupes si nécessaire. - **`vm_guest_tools`** : type d'hyperviseur / paquet cible. Les champs peuvent être **préremplis** depuis la machine (`machine.name`, IP DHCP, interface primaire détectée par `machine_probe`, utilisateur SSH), mais restent **modifiables** avant validation. ### Exemple de manifeste (attendu dans la spec) ```json { "id": "identity_network", "label": "Hostname + IP statique", "requiresConfirmation": true, "risk": "network_change", "fields": [ { "name": "newHostname", "type": "hostname", "required": true }, { "name": "domain", "type": "string", "required": true, "default": "home" }, { "name": "interfaceName", "type": "select", "required": true, "defaultFrom": "detected.primaryInterface" }, { "name": "staticAddress", "type": "ipv4_cidr", "required": true }, { "name": "gateway", "type": "ipv4", "required": true, "default": "10.0.0.1" }, { "name": "dnsNameservers", "type": "ipv4_list", "required": true, "default": ["10.0.0.1", "10.0.0.10"] }, { "name": "reconnectHost", "type": "ipv4", "required": true, "defaultFrom": "staticAddress.ip" } ] } ``` Types de champ proposés : `string`, `hostname`, `ipv4`, `ipv4_cidr`, `ipv4_list`, `select`, `bool`, `int`, `path`, `secret` (jamais sérialisé en clair, jamais envoyé à Hermes/MCP). `defaultFrom` référence une valeur détectée par `machine_probe`. --- ## 4. Templates custom attendus (pseudo-shell) Tous : `LC_ALL=C`, `DEBIAN_FRONTEND=noninteractive`, marqueurs `===SU:CUSTOM_*===`, `===SU:EXIT=N===`, sortie parsable, log brut archivé. Échec contrôlé si décision manquante. ### 4.1 `custom/bootstrap-root.sh.tpl` ```sh #!/bin/sh export LC_ALL=C export DEBIAN_FRONTEND=noninteractive echo "===SU:CUSTOM_BOOTSTRAP===" apt-get update -qq 2>&1 apt-get install -y sudo resolvconf ca-certificates curl 2>&1 CODE=$? # Ajoute l'opérateur au groupe sudo (variable de formulaire, non secret). usermod -aG sudo "{{operatorUser}}" 2>&1 || echo "WARN usermod" # Vérifie sudo su - "{{operatorUser}}" -c 'sudo -n true' 2>&1 && echo "SUDO_OK" || echo "SUDO_CHECK_PENDING" echo "===SU:EXIT=${CODE}===" ``` ### 4.2 `custom/identity-network.sh.tpl` ```sh #!/bin/sh export LC_ALL=C echo "===SU:CUSTOM_IDENTITY===" # Sauvegardes avant modification. cp -a /etc/hosts "/etc/hosts.su.bak.$(date +%s)" 2>/dev/null cp -a /etc/network/interfaces "/etc/network/interfaces.su.bak.$(date +%s)" 2>/dev/null OLD_IP="{{dhcpEndpoint}}" echo "OLD_ENDPOINT=${OLD_IP}" hostnamectl set-hostname "{{newHostname}}" 2>&1 || echo "hostname_failed" # Réécrit /etc/network/interfaces pour {{interfaceName}} en statique {{staticAddress}}. # (rendu détaillé en tâche 4 ; échoue proprement si interface absente) echo "NEW_ENDPOINT={{reconnectHost}}" echo "RECONNECT_REQUIRED=1" echo "===SU:EXIT=0===" ``` Le script **ne coupe jamais la connexion sans stratégie de reconnexion planifiée par la webapp**. Si reboot requis → mécanisme `reboot_verified`. Après application, la webapp vérifie la reconnexion sur la nouvelle IP/hostname et met à jour la machine si le retour est confirmé. Erreurs distinguées : `network_config_invalid`, `interface_not_found`, `dns_config_failed`, `reconnect_failed`, `hostname_failed`, `sudo_setup_failed`. ### 4.3 `custom/install-package-groups.sh.tpl` ```sh #!/bin/sh export LC_ALL=C export DEBIAN_FRONTEND=noninteractive echo "===SU:CUSTOM_PKGGROUPS===" # {{packages}} rendu comme liste shell-safe par le backend (jamais de vim par défaut). apt-get update -qq 2>&1 apt-get install -y {{packages}} 2>&1 CODE=$? echo "===SU:EXIT=${CODE}===" ``` ### 4.4 `custom/docker-official-debian.sh.tpl` Suit la doc officielle Docker Debian (https://docs.docker.com/engine/install/debian/, https://docs.docker.com/compose/install/linux/) : clé GPG dans `/etc/apt/keyrings`, `docker.sources`, puis paquets. ```sh #!/bin/sh 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 # docker.sources écrit selon codename détecté (non secret). apt-get update -qq 2>&1 apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 2>&1 CODE=$? usermod -aG docker "{{dockerUser}}" 2>&1 || echo "WARN docker group" mkdir -p "{{composeRoot}}" 2>&1 echo "DOCKER_GROUP_RELOGIN_REQUIRED=1" {{#rebootAfterInstall}}echo "REBOOT_REQUESTED=1"{{/rebootAfterInstall}} echo "===SU:EXIT=${CODE}===" ``` ### 4.5 `custom/sharing.sh.tpl` et `4.6 custom/vm-guest-tools.sh.tpl` Installent Samba/NFS/mDNS selon les choix (sans config dangereuse par défaut) / l'agent invité choisi. Mêmes conventions de marqueurs et d'échec contrôlé. Détail des configs renvoyé à la tâche 4. Sources citées : Docker Debian https://docs.docker.com/engine/install/debian/ · Compose plugin https://docs.docker.com/compose/install/linux/ · Debian network https://wiki.debian.org/NetworkConfiguration · Debian Handbook https://www.debian.org/doc/manuals/debian-handbook/sect.network-config · resolvconf https://packages.debian.org/stable/net/resolvconf --- ## 5. JSON canonique post-install `ExecutionResult` reçoit un bloc optionnel `postInstall` (voir `40-contrats-json.md`) listant : profils exécutés, variables **non sensibles** utilisées, fichiers modifiés, paquets installés, services activés/démarrés, reboots demandés, erreurs. Secrets/tokens **jamais** inclus (variables sérialisées, logs UI, rapports, MCP). Les changements réseau/Docker sont marqués dans le rapport Markdown avec les prochaines actions attendues (reconnexion, logout/login groupe Docker, reboot). --- ## 6. Insertion dans la webapp existante - Même mécanique que les autres actions : templates versionnés, preview, exécution SSH (`runScriptSudo`), WebSocket terminal, `executions`, rapport Markdown, log brut. - Valeurs réutilisables conservées dans `script_variables_presets` (scope `global`/`machine`/`profile`) ; état par machine dans `machine_profile_state`. Le provisioning peut être un assistant ponctuel ou stocké par machine. - Hermes peut proposer des profils ou expliquer un échec, mais ne reçoit que le JSON réduit et **ne déclenche jamais** les actions à risque sans validation webapp. - Découpage en sous-jalons indépendants : bootstrap/sudo, identité+réseau, paquets de base, Docker officiel, partage réseau, outils VM/monitoring (voir `80-sous-jalons.md`).