Compare commits
No commits in common. "6b4bea0ae9f7e873bfcd22e8e1565551432bc95c" and "14e547ce564d2a6c8b24e430225650c034e9d363" have entirely different histories.
6b4bea0ae9
...
14e547ce56
27 changed files with 814 additions and 3388 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -12,15 +12,3 @@
|
|||
- remplacement de la configuration TLP monolithique par un drop-in minimal ;
|
||||
- retrait du swapfile de 16 Gio, maintien de zram 6 Gio ;
|
||||
- suppression des références obsolètes à MangoWM et au service de batterie à 80 %.
|
||||
|
||||
## Finalisation de la reconstruction (2 octobre 2026)
|
||||
|
||||
- export privé hors dépôt et déploiement par étapes sécurisés, avec suites
|
||||
confinées conservées ;
|
||||
- préparation de disque reconstruite autour de contrôles structurés, d’une
|
||||
confirmation liée à l’identité, d’un verrou coopératif, d’étapes vérifiées
|
||||
et du sous-volume `@/.snapshots` créé sans conflit ;
|
||||
- contrôle statique du dépôt, entrée unique des tests et diagnostic de lecture
|
||||
seule avec erreurs visibles ;
|
||||
- procédure de réinstallation conditionnelle, matrice des décisions et bilan
|
||||
des vérifications physiques non réalisées.
|
||||
|
|
|
|||
100
README.md
100
README.md
|
|
@ -1,56 +1,70 @@
|
|||
# Arch System
|
||||
|
||||
Référence datée et outils de reconstruction du poste **ArchASP**. Ce dépôt
|
||||
contient des listes de paquets, manifests de services, configurations système,
|
||||
modèles, inventaires historiques et procédures. Les dotfiles, données, clés et
|
||||
connexions restent hors de ce dépôt. Les listes de juillet sont des
|
||||
**observations historiques**, pas une installation actuelle garantie.
|
||||
Configuration et outillage de reconstruction de mon poste **ArchASP**.
|
||||
|
||||
## Parcours
|
||||
Ce dépôt couvre la partie système : paquets, services, stockage, énergie, démarrage, outils root et automatisations locales. Les dotfiles complets restent dans un dépôt séparé.
|
||||
|
||||
1. Lire la [référence de reconstruction](docs/reconstruction-baseline.md) et
|
||||
régler les décisions sur réseau, Secure Boot, swaps et copies externes.
|
||||
2. Vérifier les copies indépendantes et suivre la [procédure complète sur
|
||||
papier](docs/reinstall.md), avec contrôles et arrêts à chaque phase.
|
||||
3. Sur un support live UEFI identifié seulement, utiliser
|
||||
`scripts/prepare-disk.sh --dry-run DISQUE` avant toute confirmation
|
||||
destructive. La commande sans `--dry-run` efface le disque explicitement
|
||||
choisi ; elle n’a pas été exécutée sur ArchASP dans ce travail.
|
||||
4. Installer le socle officiel, démarrer et vérifier, puis restaurer les
|
||||
fonctions du poste une à une. [deploy.sh](docs/deploy-safety.md) ne restaure
|
||||
ni le boot, ni les clés, ni les données. Tant que le pare-feu n’est pas
|
||||
arbitré, ne pas lancer `deploy.sh --all`.
|
||||
## Objectifs
|
||||
|
||||
## Contrôles locaux
|
||||
- documenter l’état réel du poste ;
|
||||
- reconstruire une installation Arch sans recopier aveuglément des UUID ou des secrets ;
|
||||
- conserver les outils personnalisés nécessaires au quotidien ;
|
||||
- distinguer les services permanents des services lancés à la demande ;
|
||||
- préparer les migrations de disque Btrfs/Limine/Snapper/Secure Boot.
|
||||
|
||||
## Organisation
|
||||
|
||||
- `scripts/backup.sh` : actualise les inventaires et les configurations suivies ;
|
||||
- `scripts/audit.sh` : contrôle rapide de l’état du système ;
|
||||
- `scripts/deploy.sh` : restauration par étapes, sans écraser automatiquement `fstab` ni le boot ;
|
||||
- `scripts/prepare-disk.sh` : préparation destructive d’un disque pour une nouvelle installation ;
|
||||
- `inventory/` : informations générées sur le poste ;
|
||||
- `manifests/` : services intentionnellement activés ;
|
||||
- `rootfs/` : configurations et helpers installables en root ;
|
||||
- `Dotfiles_ArchAP` : dépôt séparé pour les configurations, scripts et unités utilisateur ;
|
||||
- `templates/` : fichiers nécessitant une adaptation à la machine ;
|
||||
- `docs/` : procédures d’entretien, de restauration et de migration SSD.
|
||||
|
||||
## Mise à jour du dépôt
|
||||
|
||||
```bash
|
||||
bash tests/run.sh
|
||||
./scripts/check-repo.sh
|
||||
./scripts/backup.sh
|
||||
git diff --check
|
||||
git status --short
|
||||
```
|
||||
|
||||
Ce point d’entrée vérifie la syntaxe des scripts, les listes, manifests,
|
||||
placeholders et liens locaux, puis les suites confinées de déploiement,
|
||||
d’export, de préparation de disque et de diagnostic. Les tests exécutent des
|
||||
**copies** dans `bwrap`, sans réseau ni accès aux périphériques blocs hôtes.
|
||||
ShellCheck est exécuté seulement s’il est déjà disponible. Une réussite ne
|
||||
certifie ni démarrage ni restauration physique.
|
||||
Toujours relire le diff avant de commit : un inventaire généré peut révéler un paquet ajouté par erreur ou un service réactivé automatiquement.
|
||||
|
||||
`scripts/check-repo.sh` est le contrôle statique seul. `scripts/audit.sh` est
|
||||
un diagnostic **en lecture seule** de la machine où il est lancé ; il peut
|
||||
rencontrer des lectures refusées et ne doit pas servir de preuve automatique
|
||||
de disponibilité. Il n’a pas été lancé sur les données réelles dans ce lot.
|
||||
## Restauration
|
||||
|
||||
## Export privé et portée des fichiers
|
||||
Le déploiement est volontairement découpé :
|
||||
|
||||
`scripts/backup.sh` crée un candidat **privé hors Git** pour revue. Il ne met
|
||||
pas à jour automatiquement `pkglist/`, `inventory/`, `rootfs/` ou `templates/`
|
||||
et n’est ni une sauvegarde des données ni une copie hors SSD. Lire son
|
||||
[contrat](docs/backup.md) et inspecter tout candidat avant promotion ou
|
||||
publication ; les fichiers peuvent contenir des informations sensibles.
|
||||
```bash
|
||||
./scripts/deploy.sh --packages
|
||||
./scripts/deploy.sh --configs
|
||||
./scripts/deploy.sh --tools
|
||||
./scripts/deploy.sh --services
|
||||
```
|
||||
|
||||
`rootfs/` regroupe des configurations déployables **à sélectionner**, non
|
||||
une vérité courante. `manifests/` décrit des intentions datées ; `inventory/`
|
||||
conserve des observations. `templates/` contient des placeholders à remplacer
|
||||
sur la machine cible. [Architecture](docs/architecture.md),
|
||||
[maintenance](docs/maintenance.md), [migration SSD](docs/ssd-migration.md),
|
||||
[sécurité](docs/security.md) et [bilan AS-001 à AS-009](docs/audits/2026-10-02-finalisation.md)
|
||||
précisent les limites.
|
||||
Pour tout appliquer :
|
||||
|
||||
```bash
|
||||
./scripts/deploy.sh --all
|
||||
```
|
||||
|
||||
`/etc/fstab`, `limine.conf`, les clés Secure Boot et les connexions réseau ne sont jamais restaurés automatiquement.
|
||||
|
||||
## État fonctionnel visé
|
||||
|
||||
- Arch Linux, Sway/SwayFX et Wayland ;
|
||||
- Btrfs avec `@`, `@home`, `@log`, `@pkg` et `@/.snapshots` ;
|
||||
- Limine + limine-snapper-sync ;
|
||||
- Secure Boot géré par `sbctl` ;
|
||||
- zram 6 Gio, sans swapfile permanent ;
|
||||
- TLP avec seuil de charge ASUS à 60 % ;
|
||||
- Nextcloud, Proton Mail Bridge, VMware et Samba lancés uniquement à la demande.
|
||||
|
||||
## Sécurité
|
||||
|
||||
Le dépôt est public. Aucun mot de passe, token, clé privée, fichier `.netrc`, base SQLite, connexion NetworkManager ou configuration WireGuard ne doit y être ajouté.
|
||||
|
|
|
|||
|
|
@ -14,11 +14,6 @@
|
|||
|
||||
Les UUID réels sont enregistrés dans `inventory/storage/` uniquement à titre documentaire. Les modèles de réinstallation utilisent des placeholders.
|
||||
|
||||
Ce schéma de base est encore visible dans les montages relevés le 2 octobre 2026.
|
||||
`/etc/fstab` du poste comporte également un swapfile et des entrées liées à un
|
||||
espace de travail ; leur reconduction n’est pas décidée. Voir la
|
||||
[référence de reconstruction](reconstruction-baseline.md).
|
||||
|
||||
## Démarrage
|
||||
|
||||
- Limine ;
|
||||
|
|
@ -32,10 +27,7 @@ Les clés Secure Boot ne sont jamais versionnées.
|
|||
|
||||
- 16 Gio de RAM physique environ ;
|
||||
- zram de 6 Gio, priorité 100 ;
|
||||
- à la date de l’ancien inventaire, aucun swapfile permanent n’était relevé.
|
||||
|
||||
Le 2 octobre 2026, un swapfile déclaré dans `/etc/fstab` et un swap sur
|
||||
périphérique sont actifs en plus de zram. Leur rôle futur reste à qualifier.
|
||||
- aucun swapfile permanent.
|
||||
|
||||
## Énergie
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
# Finalisation logicielle et limites physiques — 2 octobre 2026
|
||||
|
||||
Ce bilan complète le [suivi historique](2026-10-02-followup.md) sans le
|
||||
réécrire. Les suites de cette mission utilisent des copies et fixtures
|
||||
confinées ; aucune collecte ou préparation réelle d’ArchASP n’a eu lieu.
|
||||
|
||||
Le point d’entrée `bash tests/run.sh` rassemble le contrôle statique et les
|
||||
harnais : 55 scénarios d’export, 40 de déploiement, 45 de préparation et
|
||||
5 de diagnostic. Pour la préparation, les commandes `lsblk` et `findmnt`
|
||||
sélectionnent explicitement leurs colonnes JSON ; `lsblk --tree` force les
|
||||
descendants nécessaires aux contrôles. Les essais incluent SATA/NVMe, un
|
||||
overlay live synthétique, swaps, holders, concurrence, partitions, signaux et
|
||||
nettoyage ciblé. La simulation ne prouve pas le résultat matériel.
|
||||
|
||||
| Constat | État de cette mission | Preuve et reste à faire |
|
||||
|---|---|---|
|
||||
| AS-001 signature/démarrage | Choix opérateur, non vérifié physiquement | La procédure recense EFI, noyaux, hook masqué et contrôles sbctl ; signatures et chaîne réelle non lues. |
|
||||
| AS-002 séquence de réinstallation | Procédure décrite, non éprouvée physiquement | `docs/reinstall.md` couvre copies, stockage, socle, chroot, Limine, contrôle et arrêt ; premier démarrage absent. |
|
||||
| AS-003 AUR/yay | Procédure décrite | Amorçage comme utilisateur non root après socle, revue PKGBUILD ; aucune construction réelle. |
|
||||
| AS-004 réseau/nftables | Choix opérateur | Variantes VPN obligatoire/facultatif et flux VM documentés ; règles effectives non lues. |
|
||||
| AS-005 disque | Correction logicielle testée en confinement | Contrôles identité/montages/swaps/holders, verrou, confirmation, `.snapshots`, étapes et reprises ; aucun disque réel testé. |
|
||||
| AS-006 swaps | Choix opérateur | Zram historique séparé des swaps de charge ; aucun swap modifié. |
|
||||
| AS-007 services | Procédure décrite | CUPS/scan, nginx local, Avahi à qualifier, VMware/Samba à la demande ; essais fonctionnels absents. |
|
||||
| AS-008 export/déploiement | Correction logicielle conservée | Suites antérieures de 55 et 40 scénarios confinés ; aucun export ni déploiement réel. |
|
||||
| AS-009 snapshots/restauration | Non vérifié physiquement | `.snapshots` cohérent avec le schéma et procédure Snapper conditionnelle ; aucun démarrage de snapshot ou retour `replace` testé. |
|
||||
|
||||
L’essai déclaré du 29 mai demeure une information historique et ne valide
|
||||
pas l’état actuel. Les commandes dépendant des versions ont été recoupées
|
||||
avec les paquets locaux (util-linux 2.42.4, btrfs-progs 7.1, parted 3.8,
|
||||
linux-lts 6.18.54, Limine 12.9.1, sbctl 0.18, Snapper 0.13.2,
|
||||
limine-snapper-sync 1.32, mkinitcpio 42.1) et les sources primaires
|
||||
[Installation Arch](https://wiki.archlinux.org/title/Installation_guide),
|
||||
[Limine](https://wiki.archlinux.org/title/Limine),
|
||||
[AUR](https://wiki.archlinux.org/title/Arch_User_Repository),
|
||||
[sbctl(8)](https://man.archlinux.org/man/sbctl.8.en),
|
||||
[snapper(8)](https://man.archlinux.org/man/snapper.8.en) et
|
||||
[Btrfs](https://btrfs.readthedocs.io/en/latest/btrfs-subvolume.html).
|
||||
Les versions sur le futur support live doivent être revérifiées.
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
# Suivi ciblé de l’audit du 2 octobre 2026
|
||||
|
||||
Lot documentaire `ARCH_SYSTEM_RECONSTRUCTION_BASELINE_V1`. Observations effectuées sur ArchASP le 2 octobre 2026, branche `arch-system-reconstruction-baseline-v1`, à partir de HEAD `14e547ce564d2a6c8b24e430225650c034e9d363`. Aucun script du dépôt ni mécanisme de démarrage/restauration n’a été exécuté. La [matrice de reconstruction](../reconstruction-baseline.md) sépare les choix futurs des observations.
|
||||
|
||||
## Niveaux de preuve et accès
|
||||
|
||||
Les fichiers présents et les unités déclarées prouvent une **configuration**, pas son exécution réussie. Les dates et entrées générées prouvent un **résultat écrit**, pas l’amorçabilité. La déclaration de l’utilisateur du **29 mai 2026** selon laquelle démarrage et restauration fonctionnaient après essai est conservée comme **essai historique déclaré**, sans valider l’état du 2 octobre.
|
||||
|
||||
Les huit lectures privilégiées autorisées (`sudo -n sbctl status`, `list-files`, `verify`, `btrfs subvolume list /`, `get-default /`, `snapper -c root list`, lecture de `/etc/snapper/configs/root`, `nft -nn list ruleset`) ont toutes répondu `sudo: a password is required`. Aucun mot de passe n’a été demandé. Le contrôle ultérieur nécessite un opérateur habilité ; son résultat devra être expurgé avant publication.
|
||||
|
||||
## AS-001 et AS-009 — carte de démarrage, signature et snapshots
|
||||
|
||||
Versions locales relevées par `pacman -Q` : `sbctl 0.18-2`, `mkinitcpio 42.1-1`, `limine 12.9.1-1`, `limine-snapper-sync 1.32.0-1`, `snapper 0.13.2-1`, `snap-pac 3.0.1-3`, `linux-lts 6.18.54-2`, `linux-zen 7.2.8.zen1-2`.
|
||||
|
||||
```text
|
||||
transaction pacman : 05-snap-pac-pre.hook puis 10-limine-snapper-lock.hook
|
||||
-> snapshot pré-transaction possible ; verrou de synchronisation
|
||||
-> installation ou mise à jour du noyau
|
||||
-> hook ALPM 90-mkinitcpio-install.hook (PostTransaction, si déclenché)
|
||||
-> presets locaux : noyau + initramfs Zen/LTS dans /boot ; UKI commentés
|
||||
-> post-hook mkinitcpio /usr/lib/initcpio/post/sbctl : signe le noyau cible
|
||||
si son contrôle de clés réussit ; ne signe pas ici l'initramfs séparé
|
||||
-> zz-snap-pac-post.hook : snapshot post-transaction possible
|
||||
|
||||
hook ALPM zz-sbctl.hook du paquet
|
||||
-> masqué par /etc/pacman.d/hooks/zz-sbctl.hook -> /dev/null
|
||||
-> son « sign-all -g » PostTransaction ne doit pas être supposé actif
|
||||
|
||||
Snapper sur la racine
|
||||
-> plugin /usr/lib/snapper/plugins/10-limine-snapper-sync
|
||||
-> synchronisation asynchrone des entrées ; cleanup appelle aussi la sync
|
||||
-> /boot/limine.conf et copies d'historique de noyau/initramfs/microcode
|
||||
-> entrée normale ou entrée de snapshot sélectionnée dans Limine
|
||||
```
|
||||
|
||||
Cette carte indique des **déclencheurs possibles**, pas une chronologie prouvée pour une transaction passée. Les hooks ALPM sont ordonnés par nom ; le fichier homonyme du répertoire prioritaire peut masquer celui du paquet par lien `/dev/null` ([alpm-hooks(5)](https://man.archlinux.org/man/alpm-hooks.5.en)). Les hooks `05-snap-pac-pre.hook` et `zz-snap-pac-post.hook` sont installés ; leurs homologues suffixés `.hook.disabled` ne sont pas des hooks actifs. `90-mkinitcpio-install.hook:34-38` invoque le helper mkinitcpio ; `/usr/bin/mkinitcpio:793-819,1240-1241` lance les post-hooks après génération sauf option contraire. `/usr/lib/initcpio/post/sbctl:6-25` choisit le noyau ou un UKI, puis appelle `sbctl sign`. Les presets `/etc/mkinitcpio.d/linux-{lts,zen}.preset` n’activent que l’image normale et commentent les UKI. L’existence du post-hook permet donc une voie de signature **du noyau**, sous conditions ; elle ne démontre ni son exécution passée, ni la signature du chargeur EFI, ni celle des copies d’historique. Le hook `zz-sbctl` masqué ne prouve pas l’absence de toute autre voie.
|
||||
|
||||
| Artefact utilisé ou présent | Producteur et emplacement logique | Copie, authentification et déclencheur identifiés | Ce qui reste non vérifié |
|
||||
|---|---|---|---|
|
||||
| EFI de repli `EFI/BOOT/BOOTX64.EFI` | Binaire PE comportant un marqueur Limine sur l’ESP ; `efibootmgr` indique cette entrée comme courante | Secure Boot est activé d’après `sbctl status` non privilégié ; fichier EFI présent, daté de mai | Signature exacte, correspondance avec le binaire démarré et mécanisme de mise à jour |
|
||||
| EFI sous `EFI/arch-limine/` | Autre binaire Limine présent sur l’ESP, différent de la copie de repli ; entrée EFI nommée Arch Linux Limine existe | Aucun hook local démontré pour le signer ou le mettre à jour | Quelle entrée doit rester disponible et quels fichiers sbctl suit |
|
||||
| Noyaux Zen/LTS dans `/boot` | Paquets noyau et mkinitcpio ; fichiers datés du 2 octobre | Post-hook sbctl installé et exécutable, conditionné par l’état des clés ; hook ALPM mkinitcpio présent | Exécution passée, signature actuelle et politique de vérification Limine |
|
||||
| Initramfs et microcode séparés dans `/boot` | mkinitcpio et paquet microcode ; référencés par `/boot/limine.conf:18-27` | Modules Limine ; aucune signature EFI directe identifiée pour ces fichiers séparés | Intégrité effective et cohérence avec le noyau chargé |
|
||||
| Copies de noyaux, initramfs et microcode pour snapshots | `limine-snapper-sync` ; chemins `limine_history/..._sha256_...` dans le menu généré | Noms avec empreinte SHA-256, déduplication/intégrité propres à l’outil selon la documentation locale | Existence et contrôle de **toutes** les copies, lien avec la chaîne Secure Boot, capacité à démarrer |
|
||||
| `limine.conf` et métadonnées de snapshots | Synchronisation Snapper/Limine ; `/boot/limine.conf` généré le 2 octobre | Section « Auto-generated » présente ; le modèle versionné garde seulement la partie non générée | Correspondance exacte entre menu, snapshots et fichiers, restauration réelle |
|
||||
|
||||
`efibootmgr` montre l’entrée courante `UEFI OS` vers le chemin de repli et une entrée `Arch Linux Limine` distincte. `/boot/limine.conf:12` définit une entrée par défaut et les lignes 14–27 référencent Zen et LTS avec noyau et modules séparés ; aucun UKI actif n’a été observé dans les presets. Le nom `_sha256_` d’une copie historique n’est pas une preuve de signature EFI. Un futur `sbctl verify` vérifierait les fichiers qu’il examine, pas la totalité des dépendances d’une entrée ni un démarrage réel ([sbctl(8)](https://man.archlinux.org/man/sbctl.8.en)).
|
||||
|
||||
Pour la synchronisation, le service `limine-snapper-sync.service` est désactivé/inactif ; ce n’est **pas** une preuve d’absence de synchronisation. Le plugin Snapper installé agit seulement sur le sous-volume `/` et lance la sync de façon asynchrone (`/usr/lib/snapper/plugins/10-limine-snapper-sync:17-18,64-102`). `snapper-cleanup.service.d/limine-snapper-override.conf:4` invoque aussi la sync après cleanup. Le journal des changements local de l’outil indique que l’observateur est optionnel depuis sa version 1.16.0. Les timers cleanup/timeline sont activés ; la configuration Snapper réellement chargée n’a pas été lue.
|
||||
|
||||
La configuration du dépôt et celle lisible sous `/etc/limine-snapper-sync.conf` concordent : racine `@`, snapshots `@/.snapshots`, `RESTORE_METHOD=replace`, `SET_SNAPSHOT_AS_DEFAULT=no` (`rootfs/etc/limine-snapper-sync.conf:110-181`). Pour la version installée, la documentation locale décrit `replace` comme la création d’un nouveau sous-volume depuis le snapshot sélectionné, remplaçant l’ancien sous-volume racine ; l’ID de sous-volume change et la relation avec les anciens snapshots n’est plus suivie comme avec `snapper rollback`. Les montages séparés `@home`, `@log` et `@pkg` ne font pas partie du sous-volume racine `@` observé ; leur contenu ne doit pas être présumé restauré par cette opération. L’effet exact sur ce poste, le sous-volume Btrfs par défaut et le retour à l’état normal restent à éprouver. Le snapshot local ne protège pas de la perte du SSD.
|
||||
|
||||
**Conclusion proportionnée :** AS-001 reste **ouvert** : hook pacman masqué confirmé, post-hook de signature du noyau confirmé, signatures et politique complète non vérifiées. AS-009 reste **ouvert** : voies de synchronisation et entrées de snapshots confirmées, démarrage/restauration actuels non vérifiés. La raison du masquage n’apparaît pas dans les documents actifs ni dans les commits ciblés du dépôt ; elle ne peut être inventée. Aucun hook n’a été réactivé.
|
||||
|
||||
## Compléments ciblés AS-002 à AS-008
|
||||
|
||||
| ID | Réévaluation au 2026-10-02 | Preuve et suite |
|
||||
|---|---|---|
|
||||
| AS-002 | **Confirmé, ouvert.** Le premier démarrage n’est toujours pas une procédure complète. | `docs/reinstall.md:29-67` énumère Limine, Snapper et sbctl sans séquence testée. Ajouter la procédure seulement après décisions sur signature, réseau et disponibilité des copies. |
|
||||
| AS-003 | **Confirmé, ouvert.** `yay` est installé sur ArchASP mais absent de l’ISO et non amorcé par `deploy.sh`. | `pkglist/aur.txt:12`, `scripts/deploy.sh:171-180`. Le noyau et le premier démarrage relèvent du socle ; l’intégration AUR/Snapper suit. Futur amorçage : outil de construction et dépendances disponibles, source et révision vérifiées, identité de l’utilisateur de construction, résultat installé et reproductible. Aucune construction ici. |
|
||||
| AS-004 | **Confirmé pour les fichiers ; exécution non vérifiée.** Le dépôt cible `wg0`, `/etc/nftables.conf` cible `wlp1s0` pour DNS, Web, trafic des VM et NAT. `wg-quick@wg0` est en échec. | Comparaison ciblée des deux fichiers ; `systemctl show`. L’un ou l’autre fichier peut changer l’accès du poste et des VM. `nftables.service` inactif n’indique pas le contenu des règles effectives. `sudo -n nft -nn list ruleset` refusé ; politique VPN/flux encore à décider. |
|
||||
| AS-005 | **Risque conditionnel inchangé.** Script destructif avec confirmation et contrôle des montages, mais sans preuve générale sur tous les usages actifs du disque ou sur une reprise après échec. | `scripts/prepare-disk.sh:15-69`. Renforcement et essais sur disques de test dans un lot ultérieur ; aucun script exécuté ici. |
|
||||
| AS-006 | **Confirmé, nuancé.** Btrfs/EFI de base conforme. Zram 6 Gio coexiste avec un swapfile 8 Gio, déclaré dans `/etc/fstab`, et un swap de 64 Gio sur périphérique ; `/etc/fstab` mentionne aussi un espace de travail et des swaps avec `nofail`. | `findmnt`, `swapon --show`, `/etc/fstab:18-25` expurgé. Le swapfile est configuré pour persister ; le périphérique est actif mais sa pérennité et son besoin restent à décider. Le nom `/swap-colmap` ne prouve pas un usage temporaire. |
|
||||
| AS-007 | **Confirmé, à qualifier.** CUPS service/socket/path activés et actifs, cohérent avec une disponibilité après démarrage ; `hplip`, `sane`, `sane-airscan` explicites. nginx activé/actif, configuration locale en boucle locale servant Trainlog ; Avahi service/socket activés/actifs, utilité exacte non établie. VMware/Samba demeurent désactivés/inactifs à la demande ; WireGuard est en échec. | `systemctl list-unit-files`, `list-units`, `pacman -Qi`, `/etc/nginx/nginx.conf:22-37`. Les unités utilisateur et données Trainlog restent hors dépôt. Tester impression, scan et proxy avant de modifier le manifest. |
|
||||
| AS-008 | **Confirmé.** `backup.sh` exporte listes explicites, inventaires, fichiers système sélectionnés et modèle Limine ; il n’exporte pas les scripts utilisateur. `deploy.sh` tolère certaines sources absentes et peut laisser un état partiel. | `scripts/backup.sh:19-221`, `scripts/deploy.sh:74-99,184-289`. Corriger maintenant seulement la documentation ; revoir le diff et les données sensibles avant toute publication ; scripts et tests isolés dans un lot ultérieur. |
|
||||
|
||||
`pacman -Qqen`/`-Qqem` donnent respectivement 251/17 noms contre 216/12 dans les exports de juillet. `opencode` et `openai-codex-desktop` sont **absents des paquets installés au relevé**, bien qu’encore listés ; `colmap` et plusieurs paquets `-debug` sont présents parmi les étrangers. `hplip`, `sane`, `sane-airscan`, `nginx`, `rclone` et `smartmontools` sont des installations natives explicites récentes à qualifier par fonction, sans import global dans `pkglist/`. Les bases de synchronisation locales sont datées des 1er–2 octobre ; `-Qm`/`-Qn` classent par rapport à ces bases et ne prouvent pas l’origine AUR ou officielle dans tous les cas ([pacman(8)](https://man.archlinux.org/man/pacman.8.en)).
|
||||
|
||||
## Travaux suivants et preuve de fermeture
|
||||
|
||||
1. **Arbitrer les points ouverts de la matrice** : déverrouille une cible de reconstruction explicite. Preuve : décisions datées sur signature, réseau, fonctions du poste, swaps et copies indépendantes.
|
||||
2. **Rédiger la procédure complète de réinstallation** : déverrouille un parcours depuis le support live jusqu’au premier démarrage, puis au retour des fonctions. Preuve : essai sur installation de test avec critères d’arrêt et de reprise.
|
||||
3. **Renforcer les scripts et les valider en isolement** : déverrouille une exécution moins fragile des étapes destructives et du déploiement. Preuve : tests de panne, reprises et diff d’export contrôlé, sans disque de production.
|
||||
4. **Essayer démarrage et restauration de façon contrôlée** : déverrouille une conclusion opérationnelle limitée à la version testée. Preuve : vérification autorisée des signatures, snapshots et règles, démarrage normal et de secours, restauration ciblée et retour normal.
|
||||
133
docs/backup.md
133
docs/backup.md
|
|
@ -1,118 +1,33 @@
|
|||
# Export système pour revue
|
||||
# Sauvegarde du dépôt
|
||||
|
||||
`scripts/backup.sh` produit un **candidat privé hors du dépôt**. La commande
|
||||
sans argument ne met plus à jour `pkglist/`, `inventory/`, `rootfs/` ni
|
||||
`templates/` suivis par Git. Elle ne sauvegarde ni données personnelles,
|
||||
dotfiles, clés Secure Boot, connexions réseau, secrets, ni fichiers Trainlog.
|
||||
La zone locale d’export n’est pas une preuve de sauvegarde hors disque ou de
|
||||
restauration possible. Ce code n’a été testé ici que sur des fixtures.
|
||||
## Actualisation
|
||||
|
||||
## Interface et destination
|
||||
|
||||
```text
|
||||
backup.sh [--output-dir REPERTOIRE_PARENT]
|
||||
backup.sh --dry-run [--output-dir REPERTOIRE_PARENT]
|
||||
backup.sh --help
|
||||
```bash
|
||||
./scripts/backup.sh
|
||||
```
|
||||
|
||||
`--output-dir` désigne un **parent** : chaque invocation crée un sous-répertoire
|
||||
unique. Par défaut, le parent est
|
||||
`${XDG_STATE_HOME:-$HOME/.local/state}/arch-system/exports`. Un
|
||||
`XDG_STATE_HOME` relatif est ignoré avec diagnostic, selon la
|
||||
[spécification XDG](https://specifications.freedesktop.org/basedir/0.8/) ;
|
||||
le chemin par défaut sous `HOME` est alors employé. Un parent explicite
|
||||
relatif est résolu depuis le répertoire courant. Les chemins sont normalisés ;
|
||||
la destination égale au dépôt ou située dedans, les alias vers celui-ci et les
|
||||
parents traversant un lien symbolique sont refusés. Un parent final existant
|
||||
doit appartenir à l’utilisateur et ne pas être ouvert au groupe ou aux autres.
|
||||
Les nouveaux répertoires sont créés privés, sans `chmod`/`chown` des ancêtres.
|
||||
Un parent contenant des espaces ou un antislash littéral reste accepté ; le
|
||||
calcul des empreintes ne dépend pas de la représentation de son nom.
|
||||
Le script exporte :
|
||||
|
||||
`--dry-run` affiche les sources, les catégories obligatoire/facultatif, le
|
||||
parent et les transformations ; il ne crée rien et ne lance ni sudo, ni
|
||||
collecteur, ni demande de mot de passe. Il ne prouve pas la réussite des
|
||||
lectures futures. La collecte refuse une invocation en root ; les fichiers de
|
||||
sortie sont écrits par l’utilisateur sous `umask 077`. Les lectures privilégiées
|
||||
utilisent `sudo -n`, un contrôle d’état strict et seulement les sources
|
||||
explicitement autorisées. Aucun résultat n’est installé avec sudo.
|
||||
- paquets officiels explicites avec `pacman -Qqen` ;
|
||||
- paquets étrangers/AUR explicites avec `pacman -Qqem` ;
|
||||
- paquets orphelins ;
|
||||
- unités systemd actives, activées et en échec ;
|
||||
- timers système et utilisateur ;
|
||||
- état Btrfs, zram, mémoire et SSD NVMe ;
|
||||
- configurations système explicitement autorisées ;
|
||||
- scripts utilisateur suivis par le dépôt.
|
||||
|
||||
## Collecteurs et états
|
||||
## Contrôle avant commit
|
||||
|
||||
| Classe | Collecteurs | Raison |
|
||||
|---|---|---|
|
||||
| Obligatoire | `pacman -Qqen`, `-Qqem` ; unités système activées, en cours et timers ; `lsblk`, `findmnt`, `/etc/fstab`, Btrfs subvolumes/usage, mémoire et swap ; `pacman.conf`, `makepkg.conf`, `mkinitcpio.conf` | Sans ces éléments, le candidat annoncé manquerait son noyau d’inventaire et de configuration. |
|
||||
| Facultatif | `pacman -Qdtq` ; unités en échec et unités utilisateur ; scrub Btrfs ; SMART de `/dev/nvme0` ; `nftables.conf`, limine-snapper-sync, TLP, zram-generator, Snapper, deux helpers root ; modèle Limine | Leur absence ou indisponibilité peut être légitime selon la machine, l’utilisateur ou le matériel. Elle reste visible dans le manifeste. |
|
||||
```bash
|
||||
git diff --check
|
||||
git diff --stat
|
||||
git status --short
|
||||
```
|
||||
|
||||
`SUCCESS` indique un fichier collecté non vide ; `EMPTY`, une commande
|
||||
réussie avec résultat vide permis ; `ABSENT`, une absence effectivement
|
||||
établie pour une source facultative ; `UNAVAILABLE`, un outil ou `nvme0`
|
||||
indisponible ; `CHECK_ERROR`, un contrôle impossible ou une réponse
|
||||
invalide ; `COLLECT_ERROR`, une commande ou lecture échouée.
|
||||
`PORTABILITY_REVIEW` marque le modèle Limine à relire, même après une
|
||||
transformation reconnue. Une erreur obligatoire interdit la finalisation ;
|
||||
une erreur facultative produit `COMPLETE_WITH_WARNINGS` si le noyau
|
||||
obligatoire a réussi. `COMPLETE` est réservé à un ensemble sans avertissement.
|
||||
En pratique, un modèle Limine présent entraîne une alerte de portabilité.
|
||||
Inspecter particulièrement :
|
||||
|
||||
Les listes sont des **observations** : « natif » ne prouve pas dans tous les
|
||||
cas « dépôt officiel », et « étranger » ne prouve pas « AUR ».
|
||||
`pacman -Qdtq` désigne les dépendances orphelines selon
|
||||
[pacman(8)](https://man.archlinux.org/man/pacman.8.en). La documentation
|
||||
primaire ne justifie pas une règle générale « code 1 = aucun orphelin » :
|
||||
une sortie vide avec code non nul reste `COLLECT_ERROR`, avec une cause à
|
||||
examiner. Les résultats vides de commandes revenues avec succès sont
|
||||
distingués. Le relevé SMART éventuel concerne **seulement** `/dev/nvme0`,
|
||||
pas tous les disques.
|
||||
|
||||
## Finalisation, manifeste et limites
|
||||
|
||||
La collecte commence dans un répertoire `.incomplete.*` privé, doté d’un
|
||||
`manifest.tsv` au statut `INCOMPLETE`. Les sorties sont d’abord reçues dans
|
||||
une zone de travail privée ; une commande qui écrit partiellement puis échoue
|
||||
ne publie pas ce fichier comme relevé valide. Après les éléments obligatoires,
|
||||
le manifeste est terminé et le répertoire est renommé localement en
|
||||
`export-*` sans remplacer un nom existant. Chaque invocation a son espace ;
|
||||
avant un renommage réussi, une erreur ou un `INT`/`TERM` conserve son reliquat
|
||||
marqué `INCOMPLETE` et annoncé. Après le renommage réussi, un `INT`/`TERM`
|
||||
conserve le candidat complet et annonce son chemin avec le diagnostic
|
||||
« finalisation acquise, terminaison interrompue ». L'exporteur reconnaît ce
|
||||
renommage par la disparition du staging et l'identité de l'objet au chemin
|
||||
final ; la simple présence d'une destination préexistante ne suffit pas.
|
||||
Une collision conserve le staging incomplet sans revendiquer cet autre export.
|
||||
Les exports antérieurs ne sont ni supprimés ni remplacés. Les codes sont `0` pour
|
||||
simulation ou candidat finalisé, `1` pour erreur, `2` pour usage invalide,
|
||||
`130`/`143` pour `INT`/`TERM`. `SIGKILL` et une coupure d’alimentation
|
||||
ne sont pas interceptés ; aucune garantie de durabilité après coupure n’est
|
||||
revendiquée.
|
||||
|
||||
Le manifeste contient version, début/fin UTC, révision source, statut global,
|
||||
état de chaque collecteur et SHA-256 des **fichiers de sortie finalisés**.
|
||||
Chaque empreinte est calculée via l'entrée standard, après vérification de
|
||||
l'ouverture du fichier et du succès de la commande ; seule une réponse de
|
||||
64 chiffres hexadécimaux est acceptée. Un échec ou une réponse malformée
|
||||
interdit la finalisation. Le manifeste ne contient pas son propre hash.
|
||||
Vérifier les empreintes et lire le contenu
|
||||
avant toute utilisation. Les lectures ne forment pas un instantané atomique :
|
||||
services et fichiers peuvent changer pendant la collecte. Les vérifications
|
||||
de chemins réduisent les risques d’alias et de liens ; elles ne remplacent pas
|
||||
une exclusion des modifications concurrentes de la machine.
|
||||
|
||||
Le modèle Limine candidat est produit **uniquement dans l’export**. La section
|
||||
commençant par `### Auto-generated by limine-snapper-sync` est retirée.
|
||||
Les formes `root=UUID=<hexadécimal/tirets>` et
|
||||
`root=PARTUUID=<hexadécimal/tirets>` deviennent des placeholders distincts.
|
||||
Une forme `root=` non reconnue est signalée ; tous les autres champs
|
||||
demeurent soumis à revue. Ni `ESP_PATH`, ni signatures, ni snapshots ne sont
|
||||
changés. Le modèle n’est pas déclaré universellement portable.
|
||||
|
||||
## Revue avant toute promotion manuelle
|
||||
|
||||
Examiner les fichiers et le manifeste hors du dépôt, particulièrement
|
||||
identifiants de stockage, noms de services, `nftables.conf`, `fstab` et
|
||||
Limine. Aucun filtrage simple ne certifie l’absence de secrets ; **ne pas
|
||||
publier automatiquement**. Les listes de déploiement et la politique réseau
|
||||
restent des décisions de reconstruction distinctes de l’état observé.
|
||||
Une éventuelle reprise manuelle dans Git demande une revue spécifique des
|
||||
contenus et de leur portée. L’existence du candidat n’apporte ni copie
|
||||
externe, ni restauration testée.
|
||||
- `pkglist/orphans.txt` ;
|
||||
- les services qui apparaissent nouvellement activés ;
|
||||
- les modifications de `nftables.conf`, `mkinitcpio.conf` et `pacman.conf` ;
|
||||
- l’absence de secrets.
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Déploiement par étapes : contrat de sécurité V1
|
||||
|
||||
Ce document décrit `scripts/deploy.sh`. Il ne valide pas une réinstallation et **n'autorise pas son exécution réelle sur ArchASP** tant que les décisions pertinentes de [reconstruction](reconstruction-baseline.md) (notamment réseau et signature) restent ouvertes. Le script ne restaure ni `/etc/fstab`, ni Limine, ni les clés Secure Boot, ni les données ou dotfiles.
|
||||
|
||||
## Options et résultat
|
||||
|
||||
`--packages`, `--configs`, `--tools`, `--services` sélectionnent explicitement des étapes ; `--all` sélectionne les quatre dans cet ordre. Plusieurs sélections peuvent être combinées. Sans sélection, le code est `2`. `-h`/`--help` affiche l'aide. `--dry-run` s'ajoute à une sélection et affiche sources, destinations et actions sans confirmation, sudo, gestionnaire de paquets, sauvegarde ni temporaire de déploiement. Il peut lire les fichiers du dépôt et interroger sans privilèges la présence des unités systemd lorsque leur installation n'est pas demandée.
|
||||
|
||||
Exemples de **plan uniquement** :
|
||||
|
||||
```bash
|
||||
./scripts/deploy.sh --dry-run --configs
|
||||
./scripts/deploy.sh --dry-run --all
|
||||
```
|
||||
|
||||
Le code `0` en simulation signifie seulement « préconditions statiques connues satisfaites et plan affiché ». Les validations nftables/sudoers privilégiées, la présence future des unités apportées par les paquets et le résultat d'une installation ne sont pas vérifiés par ce code. Le code `1` signale une précondition ou une étape échouée ; le code `2`, un usage invalide ; `130`/`143`, une interruption gérable par `INT`/`TERM`. En application, `0` signifie que les commandes sélectionnées se sont terminées sans erreur signalée **ou que l'utilisateur a répondu non à la confirmation** ; dans ce dernier cas, « Déploiement annulé » est affiché et rien n'est appliqué. Un code `0` après application ne prouve pas le démarrage ni la conformité fonctionnelle.
|
||||
|
||||
## Préconditions et ordre
|
||||
|
||||
Les fichiers de configuration, helpers et templates sélectionnés doivent exister, être lisibles, non vides et ne pas être des liens. Les listes de paquets et manifests doivent exister et être lisibles ; un fichier **présent mais vide** est une sélection volontairement vide. Les lignes vides et commentaires entiers sont ignorés ; chaque autre ligne doit contenir un seul nom valide, sans doublon ni option commençant par `-`. Une source manquante ou une ligne invalide arrête le lot avant toute application.
|
||||
|
||||
Le script doit être appelé depuis un compte non root lorsque des paquets étrangers ou `--tools` sont demandés. Il obtient le bénéficiaire avec `id`, pas avec `USER` ou `SUDO_USER`, et refuse une règle sudoers au nom de root. Il n'amorce pas `yay` : une liste étrangère non vide avec `yay` absent fait échouer le précontrôle avant l'installation native. Les noms du fichier « AUR » sont des entrées à soumettre à yay, pas une preuve qu'ils proviennent tous de l'AUR ; leur compatibilité et leur source doivent être examinées séparément.
|
||||
|
||||
Pour une application, le nom de machine est lu et validé dans `/etc/hostname` avant la confirmation et toute mutation. Un fichier absent, illisible, vide ou invalide arrête l'application avec code `1`. La commande externe `hostname` n'est pas requise. Le contrôle privilégié des cibles utilise `find` et `/usr/bin/bash`, qui doivent être disponibles ; une absence de `find` est diagnostiquée au précontrôle.
|
||||
|
||||
Après confirmation, les validations privilégiées connues sont tentées **avant la première mutation durable** : `sudo -v`, `nft -c -f` pour `--configs`, puis rendu des deux règles sudoers et `visudo -cf` pour `--tools`. Si `--packages` est également demandé et fournit `nft`, sa disponibilité et la validation sont différées après les paquets, avant la première configuration. Les unités systemd sont vérifiées immédiatement si aucun paquet n'est demandé ; sinon elles sont vérifiées après les paquets, avant leur première utilisation. Un `--all` peut donc échouer après installation des paquets : il n'est pas garanti entièrement avant la première mutation.
|
||||
|
||||
`--tools` et `--services` peuvent **arrêter** les quatre unités VMware/Samba du manifest à la demande avec `disable --now`. `--services` active les services et timers listés. Une unité requise absente est une erreur ; une unité à la demande déjà désactivée et inactive est laissée telle quelle. Les autres erreurs de désactivation ou d'activation ne sont pas masquées. `--configs` effectue `daemon-reload` et lance TLP si sa commande est disponible. Ces actions peuvent interrompre un service déjà utilisé : choisir les étapes et le moment en conséquence.
|
||||
|
||||
## Cibles, sauvegardes et reprise
|
||||
|
||||
Les cibles existantes sont copiées **avant** remplacement dans un répertoire root privé créé paresseusement par `mktemp -d` sous `/root/arch-system-restore.*`. Chaque invocation obtient un nom atomiquement unique, y compris dans la même seconde. Les fichiers remplacés sont sous `existing/` avec leur chemin d'origine et conservent leurs métadonnées par `cp -a`. Une cible initialement absente reçoit seulement un marqueur vide sous `absent/` : ce n'est pas une fausse copie de sauvegarde. Les helpers et les deux règles sudoers suivent la même politique que les configurations. Le chemin de sauvegarde est affiché en cas d'échec et à la fin.
|
||||
|
||||
Le script refuse une cible ou un parent qui est un lien symbolique, même cassé, ou une cible existante non régulière. Il réexamine les cibles avant sauvegarde et avant installation. Le contrôle privilégié renvoie, **uniquement après succès de l'inspection**, un état strict : `EXISTING`, `ABSENT`, `LINK` ou `INVALID`. Une erreur de sudo ou de l'inspection, une réponse vide ou inconnue, ou un état plausible accompagné d'un code d'échec arrêtent le lot ; aucun marqueur d'absence ni remplacement de la cible concernée n'est alors autorisé. Une erreur au second contrôle conserve la sauvegarde déjà faite. Cela limite les écritures inattendues mais ne garantit pas l'absence de toute course avec un autre processus privilégié. Les fichiers sudoers rendus sont gardés dans un répertoire temporaire privé et supprimés sur sortie normale, erreur, `INT` ou `TERM`. Les sauvegardes root ne sont jamais supprimées par ce nettoyage. Un `SIGKILL` ou une coupure de courant ne déclenche pas de trap.
|
||||
|
||||
Une erreur arrête les étapes suivantes et affiche la phase, les phases achevées, les actions déjà appliquées et le répertoire de sauvegarde disponible. Sur `INT` ou `TERM`, le bilan affiche le signal, la phase, les étapes achevées, les actions **confirmées**, l'action en cours dont les effets sont à vérifier, et le chemin des sauvegardes lorsqu'il existe. Une commande interrompue peut avoir déjà eu des effets, même si elle n'est pas dans la liste des actions confirmées. Il n'y a **aucun rollback automatique global** : pacman/yay, services et fichiers n'ont pas une annulation commune. Pour reprendre, comparer l'état réel aux cibles et aux copies ou marqueurs, restaurer manuellement ce qui doit l'être, puis relancer uniquement une étape comprise. Les installations de paquets peuvent être relancées avec `--needed`, mais cela n'annule pas une transaction partielle ; les copies et activations systemd sont répétables seulement après vérification de l'état et des effets de service.
|
||||
|
||||
## Tests autorisés
|
||||
|
||||
Commande unique du harnais :
|
||||
|
||||
```bash
|
||||
tests/deploy/test.sh
|
||||
```
|
||||
|
||||
Le harnais exige `bwrap` déjà installé et vérifie les exécutables réels qu'il place dans son `PATH` fermé ; un outil manquant échoue avant la création d'un lien cassé. Il crée un dépôt synthétique et exécute **une copie** de `deploy.sh` dans un espace sans réseau, avec `/` en lecture seule, `/etc` et `/usr/local/sbin` remplacés par des fixtures privées. La doublure `sudo` reconnaît strictement le fragment de contrôle prévu et simule son résultat sans jamais exécuter librement un argument `bash -c` ; toute autre sous-commande inconnue est refusée. Paquets, yay, nftables et systemd sont simulés. Les scénarios nominaux rejettent tout stderr ; les scénarios d'erreur contrôlent les diagnostics admis. Aucun essai réel du script du dépôt sur ArchASP n'est effectué. `bash -n` complète les tests ; ShellCheck n'est utilisé que s'il est déjà présent. Les réponses simulées prouvent uniquement les branches exercées : ni déploiement, ni pare-feu, ni signature, ni démarrage, ni restauration ne sont validés.
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
# Référence de reconstruction d’ArchASP — observations du 2 octobre 2026
|
||||
|
||||
Ce document distingue l’**intention déjà documentée**, l’**état observé** et une **proposition à décider**. Il ne constitue ni un inventaire à déployer tel quel, ni une procédure de réinstallation validée. Les preuves et limites techniques sont détaillées dans [le suivi d’audit](audits/2026-10-02-followup.md). Les exports d’`inventory/` datent principalement du 26 juillet 2026 et conservent leur valeur d’archive d’observation.
|
||||
|
||||
## Responsabilités et ordre de retour
|
||||
|
||||
1. **Premier démarrage et administration** : Arch, accès au dépôt et à ses dépendances minimales, EFI montée sur `/boot`, racine Btrfs, noyau et initramfs, entrée Limine normale, réseau et compte administrateur. Le Secure Boot documenté exige une chaîne de confiance vérifiée avant de compter sur un démarrage avec cette politique activée.
|
||||
2. **Fonctions système du poste après ce démarrage** : Snapper et entrées de secours, services voulus, impression, énergie, pare-feu et outils root. Leur présence actuelle ne les rend pas tous indispensables au premier démarrage.
|
||||
3. **Charges de travail et projets** : Trainlog, calcul scientifique, VM et stockage de travail. Le dépôt peut noter leurs dépendances système, tandis que leurs données et unités utilisateur gardent leur propre responsable.
|
||||
4. **Hors Git système** : `~/.dotfiles`, données, sauvegardes, connexions réseau, secrets, clés Secure Boot et matériel de récupération. `arch-system` ne les sauvegarde pas.
|
||||
|
||||
## Matrice de décision
|
||||
|
||||
« À décider » ne donne aucune autorisation de modifier la machine ou les manifests. Les commandes citées en preuve sont des lectures effectuées le 2 octobre 2026 ; aucun identifiant de disque ou secret n’est reproduit.
|
||||
|
||||
| Composant / fonction | Intention déjà documentée | État observé au 2026-10-02 | Preuve | Périmètre de reconstruction | Proposition | Décision encore requise | Validation attendue |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Arch, EFI, Btrfs, noyaux et Limine | Base Arch ; EFI `/boot` ; `@`, `@home`, `@log`, `@pkg`, `@/.snapshots` ; Limine Zen/LTS | Montages conformes au schéma ; deux noyaux et initramfs présents ; entrée EFI courante sur le chemin de repli `EFI/BOOT/BOOTX64.EFI` | `findmnt`, `lsblk`, `efibootmgr`, `/boot/limine.conf:14-27` | Premier démarrage | Garder le schéma comme cible structurelle ; paramétrer disque et UUID lors de l’installation | Choix du SSD cible et accès au support live | Démarrage normal depuis le nouveau SSD, montages et UUID vérifiés |
|
||||
| Secure Boot et signature | Clés personnelles gérées par sbctl | Secure Boot activé ; hook pacman `zz-sbctl.hook` masqué ; post-hook mkinitcpio sbctl présent ; signatures non vérifiées | `sbctl status` sans privilège ; lien `/etc/pacman.d/hooks/zz-sbctl.hook` ; `/usr/lib/initcpio/post/sbctl:11-25` | Chaîne de démarrage | Établir la politique pour noyaux et chargeurs EFI avant toute action | Pourquoi le hook pacman est-il masqué ? Quels fichiers doivent être signés et par quel déclencheur ? | `sbctl list-files` et `verify` autorisés, contrôle après mise à jour, démarrage Secure Boot |
|
||||
| Snapper et retour arrière | Entrées de snapshots via limine-snapper-sync | Entrées générées ; hooks `snap-pac`, plugin Snapper et appel depuis cleanup présents ; service observateur inactif ; restauration configurée `replace` | `/boot/limine.conf:29`, `/usr/share/libalpm/hooks/05-snap-pac-pre.hook`, `/usr/lib/snapper/plugins/10-limine-snapper-sync:17-18` | Secours après démarrage | Conserver ces voies comme mécanismes observés, puis vérifier leurs résultats | Politique de conservation et scénario de restauration voulu | Liste des snapshots et fichiers associés, démarrage d’essai, retour normal |
|
||||
| Paquets officiels / natifs | `pkglist/pacman.txt` = export explicite de juillet | 251 natifs explicites contre 216 dans l’export ; certains outils récents servent aux projets | `pacman -Qqen`, `comm`, `/var/lib/pacman/sync/*.db` | Base puis fonctions | Sélectionner les fonctions nécessaires avant d’éditer les listes | Quels ajouts deviennent durables sur un futur poste ? | Installation de test par étapes ; origine des paquets confirmée |
|
||||
| Paquets étrangers et `yay` | `pkglist/aur.txt` = export explicite ; `deploy.sh` attend `yay` | 17 étrangers explicites contre 12 dans l’export ; `yay` installé ici ; plusieurs `-debug` et `colmap` nouveaux | `pacman -Qqem`, `pacman -Qm`, `scripts/deploy.sh:171-180` | Après premier démarrage | Documenter l’amorçage futur de `yay` et choisir les paquets AUR utiles séparément | Source, intégrité et besoin des paquets étrangers | Construction contrôlée depuis sources vérifiées, versions et dépendances relevées |
|
||||
| Réseau et pare-feu | NetworkManager et nftables voulus ; fichier du dépôt orienté `wg0` | NetworkManager actif ; `wg-quick@wg0` en échec ; fichier nftables déployé orienté `wlp1s0` ; règles effectives non lisibles | `systemctl show`, comparaison expurgée des deux `nftables.conf` | Réseau initial puis politique du poste | Prévoir l’accès réseau minimal avant d’appliquer une politique restrictive | VPN obligatoire ou facultatif ? Quels flux du poste et des VM doivent passer par quelle interface ? | Règles effectives examinées, flux DNS/Web/VM testés sans perte d’accès |
|
||||
| Impression / numérisation | `cups.service` dans le manifest | CUPS service, socket et path activés et actifs ; `hplip`, `sane`, `sane-airscan` explicites | `systemctl list-unit-files 'cups*'`, `list-units`, `pacman -Qi` | Fonction du poste après démarrage | Documenter la disponibilité CUPS voulue au démarrage ; qualifier scanners et matériel | Périphérique et usage durable de la numérisation | Redémarrage, impression et numérisation testées |
|
||||
| nginx pour Trainlog | Non présent dans le manifest de juillet | Paquet explicite et service activé/actif ; serveur local en boucle locale avec proxy vers backend Trainlog | `pacman -Qi nginx`, `systemctl show nginx`, `/etc/nginx/nginx.conf:28-37` | Dépendance d’application après démarrage | Décrire le paquet, l’activation éventuelle, la configuration système et le backend attendu ; laisser Trainlog et ses données au projet | Cette fonction doit-elle être rétablie sur tout futur ArchASP ? Où sera conservée sa configuration nginx ? | Proxy local testé après restauration du backend, sans recopier les données ici |
|
||||
| Avahi | Non présent dans le manifest de juillet | Service et socket activés/actifs ; dépendance de paquets impression/scan observée | `systemctl list-unit-files 'avahi*'`, `pacman -Qi hplip sane sane-airscan` | Fonction du poste à qualifier | Examiner l’usage réel avant de le classer comme indispensable | Découverte réseau nécessaire à quels périphériques ? | Impression/scan et découverte testés avec la politique retenue |
|
||||
| VMware / Samba | Services lancés à la demande | Quatre services ciblés désactivés et inactifs au relevé | `manifests/on-demand-system-services.txt`, `systemctl show` | Charge de travail après démarrage | Maintenir la séparation système/helpers et commandes utilisateur | VMware et Samba restent-ils nécessaires sur le futur poste ? | Démarrage/arrêt à la demande contrôlés |
|
||||
| Zram et swaps supplémentaires | Ancien socle : zram 6 Gio, sans swapfile permanent | Zram 6 Gio ; swapfile 8 Gio déclaré dans `/etc/fstab` ; swap sur périphérique et autres entrées de travail | `swapon --show`, `/etc/fstab:18-25` expurgé | Base mémoire puis charge de travail | Conserver zram comme référence historique ; qualifier séparément les swaps ajoutés | Quels swaps doivent persister après reconstruction et pourquoi ? | Après redémarrage : capacité, priorité et dépendance au disque de travail vérifiées |
|
||||
| Outils récents | Aucun statut global dans le dépôt | `colmap`/`colmap-debug` étrangers ; `rclone`, `smartmontools` natifs explicites ; `opencode` absent malgré la liste de juillet ; `openai-codex-desktop` absent du paquet installé malgré la liste AUR de juillet | `pacman -Q`, `-Qm`, `-Qqen`, `-Qqem` | Projets / administration à qualifier | Ne pas transformer l’export de juillet en cible automatique | Usage durable, source des paquets et sauvegarde de configuration ? | Fonction testée uniquement après décision et installation ciblée |
|
||||
| Dotfiles, données et secrets | Dotfiles séparés ; secrets hors Git | `~/.dotfiles` existe ; disponibilité d’une copie externe et restauration non vérifiées | `README.md:5`, `docs/security.md`, vérification ciblée de présence | Hors `arch-system` | Préparer un accès indépendant du SSD remplacé | Où sont les copies récupérables et qui en est responsable ? | Copie datée accessible puis restauration d’essai sans publier les secrets |
|
||||
|
||||
`pacman -Qn` signifie présent dans les bases de synchronisation locales, qui étaient datées des 1er–2 octobre ; ce n’est pas une preuve universelle de dépôt « officiel » si d’autres dépôts sont configurés. `pacman -Qm` signifie absent de ces bases, pas nécessairement construit depuis l’AUR. Le `pacman.conf` lisible ne montre que `core`, `extra` et `multilib` activés au relevé.
|
||||
|
||||
Pour Trainlog, le minimum système observé est le paquet nginx, son activation,
|
||||
un serveur écoutant uniquement en boucle locale, un nom local résolu sur cette
|
||||
boucle et un proxy vers le backend. La configuration nginx effective n’est pas
|
||||
suivie par `rootfs/` ; sa destination de sauvegarde et sa forme portable restent
|
||||
à décider. Le backend, ses unités utilisateur et ses données relèvent du
|
||||
projet et des dotfiles, pas de cette configuration système. Pour l’impression,
|
||||
CUPS est disponible par service, socket et path ; la nécessité d’Avahi pour le
|
||||
périphérique utilisé reste à établir par un essai ciblé.
|
||||
|
||||
## Dépendances avant perte ou remplacement du SSD
|
||||
|
||||
| Élément | Responsable | Preuve nécessaire avant l’opération | État établi dans ce lot |
|
||||
|---|---|---|---|
|
||||
| Dépôt `arch-system` | Copie indépendante du SSD courant | Destination accessible, révision datée et récupération d’essai | Remote configuré seulement ; disponibilité et révision distante non vérifiées |
|
||||
| `~/.dotfiles` et commandes utilisateur | Dépôt utilisateur séparé | Copie indépendante et méthode de restauration | Répertoire local présent ; copie externe non vérifiée |
|
||||
| Données personnelles et projets, dont Trainlog | Sauvegarde de données hors Git | Destination, date, intégrité et restauration ciblée | Non vérifiées ; aucun contenu personnel lu |
|
||||
| Connexions réseau, secrets, clés et politique Secure Boot | Conservation sécurisée hors dépôt public | Support récupérable, droits et procédure de réenrôlement/récupération | Non vérifiés ; aucune clé consultée |
|
||||
| Support live et accès de secours | Matériel de récupération | Démarrage du support avec la politique Secure Boot retenue, réseau et accès aux copies | Support décrit dans `docs/ssd-migration.md`, disponibilité actuelle non vérifiée |
|
||||
|
||||
Pour chaque sauvegarde, distinguer quatre preuves : **destination mentionnée**, **destination accessible**, **copie datée constatée**, **restauration testée**. Aucune de ces étapes ne découle automatiquement de l’existence d’un remote Git ou d’un snapshot local.
|
||||
|
||||
## Décisions ouvertes à regrouper avant la procédure complète
|
||||
|
||||
1. Politique voulue pour la signature des noyaux, des chargeurs EFI et des copies historiques ; raison du masquage du hook pacman sbctl.
|
||||
2. Politique réseau : VPN, flux directs, trafic des VM et comportement lorsque `wg0` est indisponible.
|
||||
3. Fonctions durables du futur poste : impression/scan, nginx/Trainlog, Avahi, VMware/Samba et outils de projet.
|
||||
4. Rôle et persistance des swaps supplémentaires, ainsi que destination vérifiable des copies hors SSD.
|
||||
|
||||
Ces arbitrages précèdent toute réécriture de `pkglist/`, `manifests/`, `rootfs/` ou des scripts.
|
||||
|
||||
## État du dépôt après finalisation logicielle
|
||||
|
||||
La [procédure de réinstallation](reinstall.md) décrit maintenant les commandes,
|
||||
préconditions, contrôles et arrêts. Le [bilan](audits/2026-10-02-finalisation.md)
|
||||
distingue corrections logicielles simulées, choix opérateur et preuves
|
||||
physiques manquantes. Cette mise à jour ne modifie ni les listes de juillet,
|
||||
ni les manifests, ni la politique Secure Boot/réseau. AS-001 et AS-009
|
||||
restent ouverts tant que signatures, amorçage et restauration actuels ne sont
|
||||
pas observés sur la cible. L’essai déclaré du 29 mai reste historique.
|
||||
|
|
@ -1,280 +1,67 @@
|
|||
# Réinstallation d’ArchASP — procédure conditionnelle
|
||||
# Réinstallation complète
|
||||
|
||||
Cette procédure est un **parcours sur papier** : aucun SSD neuf, premier
|
||||
redémarrage, retour de snapshot ou restauration externe n’a été essayé dans ce
|
||||
lot. Lire la [référence de reconstruction](reconstruction-baseline.md) et le
|
||||
[bilan de finalisation](audits/2026-10-02-finalisation.md). Les commandes
|
||||
marquées « live » s’exécutent depuis un support Arch UEFI vérifié ; celles
|
||||
marquées « chroot » dans `arch-chroot /mnt` ; celles marquées « utilisateur »
|
||||
après le premier démarrage. **Arrêter à chaque contrôle manquant**. Ne jamais
|
||||
copier les exemples en bloc.
|
||||
## 1. Préparer le disque
|
||||
|
||||
## 1. Copies et moyens de retour — avant de toucher au SSD
|
||||
|
||||
**Contexte : poste actuel, utilisateur.** Obtenir une copie indépendante du SSD
|
||||
à remplacer de `arch-system`, des dotfiles séparés et des données personnelles,
|
||||
dont Trainlog. Conserver les moyens de récupération de clés, réseau et Secure
|
||||
Boot hors Git public. Pour chaque copie noter : support, date, empreinte et un
|
||||
fichier récupéré lors d’un essai. Un remote déclaré, un export privé de
|
||||
`backup.sh` et un snapshot sur le même SSD ne sont pas des restaurations
|
||||
testées. **Arrêt** si l’un de ces accès est absent ; les sauvegardes ne sont pas
|
||||
réalisées par cette procédure.
|
||||
|
||||
## 2. Support live et cible — live, avant toute mutation
|
||||
|
||||
Démarrer le support Arch en UEFI et vérifier `test -d /sys/firmware/efi`.
|
||||
Contrôler le réseau de secours et l’accès aux copies indépendantes sans
|
||||
publier de secrets. Installer sur le support live uniquement les dépendances
|
||||
manquantes **après choix explicite de l’opérateur** ; la présente mission n’en
|
||||
a installé aucune. Le script exige Bash, Python 3, `lsblk`, `findmnt`,
|
||||
`swapon`, `readlink`, `wipefs`, `parted`, `partprobe`, `udevadm`, `mkfs.fat`,
|
||||
`mkfs.btrfs`, `btrfs`, `mount` et `umount`. La suite demande aussi `pacstrap`,
|
||||
`genfstab`, `arch-chroot` et Neovim. Vérifier leur présence et leur version
|
||||
sur le support réellement démarré ; arrêter si l’éditeur ou un outil requis
|
||||
manque.
|
||||
|
||||
Identifier manuellement la cible, le support live, le dépôt et les sauvegardes :
|
||||
Depuis l’ISO Arch :
|
||||
|
||||
```bash
|
||||
lsblk -e7 -o PATH,TYPE,MAJ:MIN,SIZE,MODEL,SERIAL,WWN,FSTYPE,MOUNTPOINTS
|
||||
findmnt -o SOURCE,TARGET,FSTYPE
|
||||
swapon --show
|
||||
sudo ./scripts/prepare-disk.sh /dev/nvme0n1
|
||||
```
|
||||
|
||||
Aucun disque par défaut n’est fourni. Renseigner une variable dans **ce shell**
|
||||
après recoupement de l’identité physique ; un nom `/dev/nvme*` seul ne suffit
|
||||
pas :
|
||||
La commande est destructive et exige une confirmation explicite.
|
||||
|
||||
## 2. Installer la base
|
||||
|
||||
Exemple :
|
||||
|
||||
```bash
|
||||
: "${DISK:?Renseigner le disque entier confirmé avant toute suite}"
|
||||
./scripts/prepare-disk.sh --dry-run "$DISK"
|
||||
pacstrap -K /mnt base linux-lts linux-firmware amd-ucode btrfs-progs networkmanager git sudo neovim
|
||||
```
|
||||
|
||||
Le mode simulation n’écrit rien, ne confirme rien et ne prouve pas qu’un
|
||||
contrôle privilégié inaccessible a réussi. Un overlay live n’est admis que si
|
||||
son support physique apparaît séparément sous `/run/archiso/bootmnt`, reste
|
||||
distinct de la cible et que chaque couche déclarée renvoie à ce support ou
|
||||
à un `tmpfs` ; les overlays non attribuables et les Btrfs
|
||||
multi-périphériques ambigus sont refusés. Dans ce cas **arrêt et revue de la
|
||||
provenance du support**, pas de contournement. Examiner `/mnt`, les montages,
|
||||
swaps et holders. Ne pas démonter ou désactiver une utilisation pour faire
|
||||
passer le contrôle.
|
||||
|
||||
## 3. Préparation — live, destructive et humaine
|
||||
|
||||
Après les copies vérifiées et une seconde lecture de l’identité affichée :
|
||||
Générer ensuite le `fstab` depuis les montages réels :
|
||||
|
||||
```bash
|
||||
: "${DISK:?Cible obligatoire}"
|
||||
./scripts/prepare-disk.sh "$DISK"
|
||||
genfstab -U /mnt >> /mnt/etc/fstab
|
||||
```
|
||||
|
||||
Le script exige root dans un contexte live Arch UEFI, un verrou coopératif et
|
||||
la saisie exacte `ERASE <chemin-canonique> <major:minor>`. Il revérifie la cible
|
||||
avant `wipefs`. Résultat attendu : GPT, ESP FAT32 d’environ 1 Gio montée sur
|
||||
`/mnt/boot`, Btrfs pour le reste et montages `@`, `@home`, `@log`, `@pkg`,
|
||||
`@/.snapshots`. Vérifier les chemins réels avec `findmnt -R /mnt` et
|
||||
`lsblk -f "$DISK"`. **Arrêt** si une étape échoue : le partitionnement est
|
||||
irréversible, non transactionnel ; la dernière commande peut avoir eu un
|
||||
effet même avec code non nul. Lire le bilan des étapes et les montages restants,
|
||||
inspecter manuellement, ne jamais relancer aveuglément.
|
||||
Ne jamais recopier directement l’ancien `fstab`.
|
||||
|
||||
## 4. Socle officiel — live, après montages vérifiés
|
||||
## 3. Configuration de base
|
||||
|
||||
Vérifier la disponibilité des paquets sur le miroir configuré et le processeur
|
||||
avant `pacstrap`. Pour la variante AMD observée, le socle proposé est :
|
||||
Après `arch-chroot /mnt` :
|
||||
|
||||
- fuseau horaire, locale et hostname ;
|
||||
- utilisateur et sudo ;
|
||||
- réseau ;
|
||||
- Limine ;
|
||||
- initramfs ;
|
||||
- Secure Boot ;
|
||||
- Snapper.
|
||||
|
||||
## 4. Cloner le dépôt
|
||||
|
||||
```bash
|
||||
pacman -Si base linux-lts linux-firmware amd-ucode btrfs-progs dosfstools \
|
||||
networkmanager git sudo neovim limine mkinitcpio efibootmgr
|
||||
pacstrap -K /mnt base linux-lts linux-firmware amd-ucode btrfs-progs \
|
||||
dosfstools networkmanager git sudo neovim limine mkinitcpio efibootmgr
|
||||
git clone https://git.labfytools.com/fy59/arch-system
|
||||
cd arch-system
|
||||
```
|
||||
|
||||
Pour un autre CPU, choisir le microcode correspondant et revalider la ligne.
|
||||
Le noyau, l’initramfs, l’ESP, Limine et le réseau minimal doivent suffire au
|
||||
premier démarrage sans `yay` ni liste AUR historique. **Arrêt** si un paquet
|
||||
ou la signature du miroir n’est pas disponible ; ne pas substituer au hasard.
|
||||
|
||||
## 5. `fstab` — live, depuis les montages réels
|
||||
|
||||
Générer d’abord un candidat, le relire puis **remplacer** un nouveau fichier,
|
||||
jamais `>>` sur un `fstab` existant :
|
||||
## 5. Restaurer par étapes
|
||||
|
||||
```bash
|
||||
genfstab -U /mnt > /mnt/etc/fstab.candidate
|
||||
nvim /mnt/etc/fstab.candidate
|
||||
findmnt -R /mnt
|
||||
./scripts/deploy.sh --packages
|
||||
./scripts/deploy.sh --configs
|
||||
./scripts/deploy.sh --tools
|
||||
./scripts/deploy.sh --services
|
||||
```
|
||||
|
||||
Vérifier une seule entrée pour `/`, `/home`, `/var/log`,
|
||||
`/var/cache/pacman/pkg`, `/.snapshots` et `/boot`, leurs UUID et options.
|
||||
Si `/mnt/etc/fstab` existe déjà, ouvrir aussi ce fichier avec Neovim et
|
||||
comparer les deux versions ; ne pas écraser une configuration inconnue.
|
||||
Après revue seulement : `mv /mnt/etc/fstab.candidate /mnt/etc/fstab`.
|
||||
Comparer aux [modèles](../templates/fstab.btrfs) comme schéma, sans copier
|
||||
l’ancien `fstab` ni ses swaps/espaces de travail. **Arrêt** si doublon ou
|
||||
UUID incohérent.
|
||||
## 6. Finaliser manuellement
|
||||
|
||||
## 6. Chroot et identité — live puis chroot root
|
||||
|
||||
Depuis le live, rendre le dépôt de reconstruction disponible dans le nouveau
|
||||
système à partir de la copie indépendante **déjà vérifiée** ; ne pas présumer
|
||||
qu’un remote sera accessible :
|
||||
|
||||
```bash
|
||||
: "${REPO_SOURCE:?Chemin de la copie indépendante vérifiée obligatoire}"
|
||||
test -f "$REPO_SOURCE/templates/limine.conf"
|
||||
test ! -e /mnt/root/arch-system
|
||||
cp -a "$REPO_SOURCE" /mnt/root/arch-system
|
||||
```
|
||||
|
||||
Entrer par `arch-chroot /mnt` après vérification des montages et du dépôt.
|
||||
Dans le chroot,
|
||||
choisir explicitement fuseau, locale, nom de machine et identités ; exemple à
|
||||
adapter :
|
||||
|
||||
```bash
|
||||
ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime
|
||||
hwclock --systohc
|
||||
nvim /etc/locale.gen
|
||||
locale-gen
|
||||
nvim /etc/locale.conf
|
||||
nvim /etc/hostname
|
||||
nvim /etc/hosts
|
||||
useradd -m -G wheel -s /bin/bash NOM_UTILISATEUR
|
||||
passwd NOM_UTILISATEUR
|
||||
passwd
|
||||
EDITOR=nvim visudo
|
||||
systemctl enable NetworkManager.service
|
||||
```
|
||||
|
||||
Dans `visudo`, n’accorder à `wheel` que la règle minimale choisie. Vérifier
|
||||
`getent passwd NOM_UTILISATEUR`, `visudo -c`, les locales produites et les
|
||||
fichiers édités. Le réseau de secours doit rester utilisable sans politique
|
||||
nftables non décidée. **Arrêt** si l’identité, le mot de passe ou le réseau ne
|
||||
sont pas contrôlables. Les commandes de construction AUR se feront plus tard
|
||||
comme utilisateur non root.
|
||||
|
||||
## 7. Initramfs et Limine — chroot root
|
||||
|
||||
Vérifier d’abord les artefacts **effectivement fournis** par les paquets :
|
||||
|
||||
```bash
|
||||
pacman -Ql linux-lts limine amd-ucode
|
||||
ls -l /usr/share/limine/BOOTX64.EFI /boot/vmlinuz-linux-lts
|
||||
findmnt /boot
|
||||
blkid
|
||||
mkinitcpio -P
|
||||
ls -l /boot/initramfs-linux-lts.img /boot/amd-ucode.img
|
||||
```
|
||||
|
||||
Vérifier l’ESP réelle sous `/boot` et les UUID relevés. Après avoir confirmé
|
||||
qu’aucun fichier préexistant ne serait écrasé, déployer le binaire fourni par
|
||||
le paquet et rédiger la configuration sur l’ESP :
|
||||
|
||||
```bash
|
||||
test "$(findmnt -nro TARGET /boot)" = /boot
|
||||
test -f /usr/share/limine/BOOTX64.EFI
|
||||
test ! -e /boot/EFI/arch-limine/BOOTX64.EFI
|
||||
install -Dm0644 /usr/share/limine/BOOTX64.EFI /boot/EFI/arch-limine/BOOTX64.EFI
|
||||
cp /root/arch-system/templates/limine.conf /boot/limine.conf.candidate
|
||||
nvim /boot/limine.conf.candidate
|
||||
```
|
||||
|
||||
Dans Neovim, remplacer l’UUID racine, supprimer l’entrée Zen si `linux-zen`
|
||||
n’a pas été installé, choisir `default_entry` correspondant à l’entrée LTS,
|
||||
et contrôler `rootflags=subvol=@`. Vérifier l’absence de `@ROOT_UUID@`,
|
||||
l’existence de chaque `kernel_path`/`module_path` et la syntaxe de la version
|
||||
Limine installée avant `mv /boot/limine.conf.candidate /boot/limine.conf`.
|
||||
Pour l’entrée UEFI, fournir séparément le **disque entier de l’ESP** et son
|
||||
numéro de partition, puis vérifier `efibootmgr -v` :
|
||||
|
||||
```bash
|
||||
: "${DISK:?Disque de l’ESP vérifié obligatoire}"
|
||||
: "${ESP_PART_NUMBER:?Numéro de partition EFI vérifié obligatoire}"
|
||||
efibootmgr --create --disk "$DISK" --part "$ESP_PART_NUMBER" \
|
||||
--label 'Arch Linux Limine' --loader '\EFI\arch-limine\BOOTX64.EFI' --unicode
|
||||
efibootmgr -v
|
||||
```
|
||||
|
||||
La [documentation Limine](https://wiki.archlinux.org/title/Limine) distingue
|
||||
le chemin UEFI et le fichier de configuration. Ne pas modifier l’EFI pendant
|
||||
cette mission. Garder une entrée LTS normale et un retour au support live.
|
||||
**Arrêt** si un artefact, UUID, numéro de partition ou chemin diffère ; aucun
|
||||
démarrage n’a été éprouvé ici.
|
||||
|
||||
## 8. Secure Boot — décision conditionnelle, chroot/firmware par opérateur
|
||||
|
||||
Avant activation, déterminer la politique de clés et les moyens de récupération
|
||||
existants. Si cette politique retient `sbctl`, vérifier sa disponibilité dans
|
||||
les dépôts (`pacman -Si sbctl`) et l’installer après décision
|
||||
(`pacman -S sbctl`). Avec la version réellement présente de `sbctl`, vérifier les
|
||||
commandes `status`, `list-files` et `verify`, les fichiers EFI suivis, le
|
||||
chargeur effectivement sélectionné, les noyaux et les copies historiques.
|
||||
Comparer les signatures au chemin de confiance Limine retenu. Le hook ALPM
|
||||
`zz-sbctl.hook` était masqué sur ArchASP ; **ne pas le réactiver du seul fait
|
||||
de ce masquage**. Le post-hook mkinitcpio observé ne démontre que la voie
|
||||
conditionnelle du noyau, pas une signature de toute la chaîne. Ne pas créer
|
||||
ni enrôler de clés, supprimer des clés firmware ou imposer `ESP_PATH` par
|
||||
habitude. **Arrêt** si les clés, fichiers à signer et déclencheurs ne sont pas
|
||||
établis ; procéder avec le support live de secours. Voir
|
||||
[la sécurité](security.md) et [sbctl(8)](https://man.archlinux.org/man/sbctl.8.en).
|
||||
|
||||
## 9. Avant et après le premier démarrage — opérateur
|
||||
|
||||
Avant de quitter le live : `findmnt -R /mnt`, `cat /mnt/etc/fstab`, présence
|
||||
du noyau/initramfs et du chargeur sur l’ESP, entrée normale Limine, accès
|
||||
réseau de secours et copies. Démonter uniquement après revue et sortir du
|
||||
chroot. Le **redémarrage humain n’a pas été fait ici**. Après démarrage normal,
|
||||
vérifier `findmnt`, `lsblk -f`, `systemctl --failed`, réseau, heure, entrée
|
||||
réellement amorcée et, si choisie, la chaîne Secure Boot. Revenir au support
|
||||
live en cas d’échec ; ne pas confondre présence de fichiers et démarrage réussi.
|
||||
|
||||
## 10. AUR et `yay` — utilisateur non root, après démarrage
|
||||
|
||||
Installer `base-devel` et `git` depuis les dépôts officiels, choisir une
|
||||
source AUR explicite et vérifier `PKGBUILD`, fichiers annexes, provenance,
|
||||
révision et dépendances. Comme utilisateur ordinaire, dans un répertoire de
|
||||
travail privé : `git clone https://aur.archlinux.org/yay.git`, examiner avec
|
||||
`nvim PKGBUILD`, puis seulement `makepkg -si`. Vérifier la version et la
|
||||
provenance installées. Ne pas lancer `makepkg` en root ni `curl | sh`.
|
||||
`deploy.sh --packages` refuse actuellement la liste AUR sans `yay` ; ce refus
|
||||
ne réalise pas l’amorçage. Sélectionner les paquets étrangers nécessaires
|
||||
individuellement, sans importer toute la liste de juillet.
|
||||
|
||||
## 11. Snapper et retour d’essai — root après premier démarrage
|
||||
|
||||
Installer d’abord `snapper` et `snap-pac` depuis leurs sources officielles
|
||||
vérifiées (`pacman -Si snapper snap-pac`, puis `pacman -S snapper snap-pac`).
|
||||
`limine-snapper-sync` était classé étranger sur ArchASP ; le sélectionner,
|
||||
examiner sa provenance et le construire comme utilisateur non root seulement
|
||||
après l’amorçage AUR du §10. Le schéma crée déjà `@/.snapshots` : vérifier
|
||||
`findmnt /.snapshots`, `btrfs subvolume show /.snapshots` et la version Snapper.
|
||||
Ne pas lancer `snapper create-config /` sur un chemin existant en supposant
|
||||
qu’il l’écrasera. Pour cette topologie, préparer la configuration `root` à
|
||||
partir du modèle de la version installée (`/usr/share/snapper/config-templates/default`
|
||||
observé ici), renseigner `SUBVOLUME="/"`, `FSTYPE="btrfs"`, enregistrer
|
||||
`SNAPPER_CONFIGS="root"` dans `/etc/conf.d/snapper` et vérifier
|
||||
`snapper -c root get-config`/`list`. N’écraser aucun fichier préexistant sans
|
||||
comparaison dans Neovim. Choisir ensuite les timers cleanup/timeline et hooks
|
||||
réellement disponibles ; vérifier leur déclenchement et les entrées
|
||||
limine-snapper-sync générées. Un retour de snapshot `replace` recrée la racine
|
||||
`@` avec un nouvel identifiant ; il ne restaure pas `@home`, `@log`, `@pkg` ni
|
||||
les données hors SSD. Faire un essai de démarrage sur snapshot et de retour
|
||||
normal **avant** de déclarer cette voie opérationnelle ; aucun essai ici.
|
||||
|
||||
## 12. Fonctions du poste — utilisateur puis root ciblé
|
||||
|
||||
Restaurer les dotfiles depuis leur copie indépendante, puis sélectionner les
|
||||
fonctions nécessaires : TLP et seuil de batterie, CUPS/impression et
|
||||
numérisation, éventuellement Avahi selon la découverte réellement requise,
|
||||
nginx local en boucle locale pour le proxy Trainlog, puis VMware/Samba à la
|
||||
demande. Pour Trainlog, ce dépôt ne transporte que les dépendances système ;
|
||||
backend, unité utilisateur et base restent dans leurs périmètres séparés.
|
||||
Vérifier chaque fonction après installation, sans activer tous les services
|
||||
historiques. Zram 6 Gio est le socle historique ; swaps additionnels et
|
||||
espaces de travail demandent une décision de charge séparée. Ne pas copier
|
||||
`rootfs/etc/nftables.conf` ni lancer `deploy.sh --all` tant que la politique
|
||||
VPN/flux n’est pas décidée. Voir [les choix réseau et services](security.md),
|
||||
[la matrice](reconstruction-baseline.md) et [le déploiement](deploy-safety.md).
|
||||
- adapter `templates/fstab.btrfs` aux nouveaux UUID ;
|
||||
- adapter `templates/limine.conf` à l’UUID Btrfs ;
|
||||
- lancer `mkinitcpio -P` ;
|
||||
- installer et configurer Limine ;
|
||||
- créer la configuration Snapper ;
|
||||
- lancer limine-snapper-sync ;
|
||||
- enrôler puis signer avec `sbctl` ;
|
||||
- restaurer les dotfiles séparément ;
|
||||
- tester `nftables`, SSH, audio, veille et seuil de batterie avant le premier usage normal.
|
||||
|
|
|
|||
|
|
@ -13,23 +13,3 @@ Le dépôt est public. Ne jamais y stocker :
|
|||
- sauvegardes brutes de `/root`.
|
||||
|
||||
Les règles sudoers fournies sont limitées à des helpers root contrôlés. Ne jamais autoriser sans mot de passe `/usr/bin/systemctl`, `/usr/bin/tee`, un shell ou un éditeur généraliste.
|
||||
|
||||
## Décisions avant restauration du réseau et de la chaîne de démarrage
|
||||
|
||||
Le fichier nftables versionné vise `wg0` ; le relevé du poste visait
|
||||
`wlp1s0`, avec WireGuard en échec. Deux politiques sont possibles, à choisir
|
||||
explicitement : **VPN obligatoire** (bloquer le trafic non autorisé quand le
|
||||
tunnel tombe, tout en prévoyant les flux de connexion et de secours) ou
|
||||
**VPN facultatif** (autoriser des flux directs définis et surveiller la panne
|
||||
du tunnel). Pour chacune, dresser une table DNS/Web/SSH, boucle locale,
|
||||
impression et flux VM, puis tester la coupure du VPN et le retour du réseau
|
||||
sans perdre l’accès d’administration. Avant ce choix, ne pas copier le
|
||||
pare-feu versionné et ne pas utiliser `deploy.sh --all`.
|
||||
|
||||
Pour Secure Boot, comparer les clés conservées hors Git, l’ESP réellement
|
||||
montée, le chargeur EFI utilisé, les noyaux et les fichiers suivis par
|
||||
`sbctl list-files`/`verify` selon la version installée. Le hook pacman sbctl
|
||||
masqué sur ArchASP ne suffit pas à justifier sa réactivation ; la signature
|
||||
éventuelle par post-hook mkinitcpio ne prouve pas la chaîne complète. Ne pas
|
||||
recréer/enrôler de clés ou changer `ESP_PATH` sans la décision de l’opérateur
|
||||
et un chemin de secours démarrable. Voir [la procédure](reinstall.md).
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
# Migration du SSD 512 Go vers 1 To — scénario historique
|
||||
|
||||
Ce document décrit un scénario de clonage antérieur. Les modèles et noms ci-dessous
|
||||
ne sélectionnent **aucun** disque actuel. La [procédure de réinstallation](reinstall.md)
|
||||
fait foi pour une préparation neuve ; pour un clonage, recouper source et cible,
|
||||
copies indépendantes et démarrage de secours avant toute commande.
|
||||
# Migration du SSD 512 Go vers 1 To
|
||||
|
||||
## Support de secours
|
||||
|
||||
|
|
@ -13,10 +8,7 @@ Carte SD Ventoy contenant :
|
|||
- Clonezilla Live ;
|
||||
- GParted Live.
|
||||
|
||||
Avec des clés Secure Boot personnelles, tester d’abord la compatibilité du
|
||||
support choisi et prévoir le retour à l’état initial. Une modification de la
|
||||
politique firmware relève de l’opérateur ; ne jamais supprimer les clés pour
|
||||
faire passer un essai de support live.
|
||||
Avec des clés Secure Boot personnelles, désactiver temporairement Secure Boot pour démarrer Ventoy sans supprimer les clés du firmware.
|
||||
|
||||
## Avant le clonage
|
||||
|
||||
|
|
@ -30,8 +22,7 @@ Commandes de contrôle :
|
|||
|
||||
```bash
|
||||
lsblk -p -o NAME,MODEL,SIZE,TRAN,FSTYPE,MOUNTPOINTS
|
||||
: "${SOURCE_DISK:?Identifier et vérifier la source avant lecture SMART}"
|
||||
smartctl -a "$SOURCE_DISK"
|
||||
sudo smartctl -a /dev/nvme0n1
|
||||
```
|
||||
|
||||
## Clonezilla
|
||||
|
|
@ -44,8 +35,7 @@ Beginner
|
|||
disk_to_local_disk
|
||||
```
|
||||
|
||||
Source et cible historiques : Samsung 512 Go et Fanxiang 1 To. Leur identité
|
||||
actuelle doit être établie à nouveau par modèle, capacité et identifiant stable.
|
||||
Source : Samsung 512 Go. Cible : Fanxiang 1 To.
|
||||
|
||||
La cible est entièrement effacée.
|
||||
|
||||
|
|
@ -67,12 +57,3 @@ sudo btrfs filesystem usage /
|
|||
```
|
||||
|
||||
Garder le Samsung intact plusieurs jours avant de le reformater comme SSD externe.
|
||||
|
||||
## Point d’arrêt actuel
|
||||
|
||||
La [réinstallation structurée](reinstall.md) remplace toute utilisation de ce
|
||||
document historique comme recette directe de partitionnement. Les copies
|
||||
indépendantes et leur récupération d’essai, le SSD cible, la politique réseau
|
||||
et Secure Boot doivent être établis avant une migration. Le script de
|
||||
préparation corrigé n’a été testé que sur des copies confinées ; aucun disque
|
||||
réel n’a été préparé dans cette finalisation.
|
||||
|
|
|
|||
114
scripts/audit.sh
114
scripts/audit.sh
|
|
@ -1,65 +1,55 @@
|
|||
#!/usr/bin/env bash
|
||||
# Diagnostic de lecture seule. Les échecs de lecture restent visibles.
|
||||
set -uo pipefail
|
||||
status=0
|
||||
|
||||
set -u
|
||||
|
||||
section() { printf '\n=== %s ===\n' "$1"; }
|
||||
read_command() {
|
||||
if ! "$@"; then printf '[!] Lecture échouée : %s\n' "$1" >&2; status=1; fi
|
||||
}
|
||||
top_processes() {
|
||||
if ! ps -eo pid,comm,%cpu,%mem,rss "--sort=$1" | sed -n '1,20p'; then
|
||||
printf '[!] Lecture échouée : ps (%s)\n' "$1" >&2
|
||||
status=1
|
||||
fi
|
||||
}
|
||||
service_state() {
|
||||
local scope="$1" unit="$2" enabled active rc=0
|
||||
local prefix=(systemctl)
|
||||
[[ "$scope" == user ]] && prefix+=(--user)
|
||||
enabled="$("${prefix[@]}" is-enabled "$unit" 2>/dev/null)" || rc=$?
|
||||
case "$enabled" in enabled|disabled|static|masked|indirect|generated|linked|linked-runtime|alias|enabled-runtime|transient) ;;
|
||||
*) enabled="ERREUR($rc)"; status=1 ;;
|
||||
esac
|
||||
rc=0
|
||||
active="$("${prefix[@]}" is-active "$unit" 2>/dev/null)" || rc=$?
|
||||
case "$active" in active|inactive|failed|activating|deactivating|reloading|maintenance) ;;
|
||||
*) active="ERREUR($rc)"; status=1 ;;
|
||||
esac
|
||||
printf '%-36s enabled=%-16s active=%s\n' "$unit" "$enabled" "$active"
|
||||
}
|
||||
section CHARGE; read_command uptime
|
||||
section MÉMOIRE; read_command free -h; read_command swapon --show
|
||||
section PRESSION
|
||||
for file in /proc/pressure/cpu /proc/pressure/memory /proc/pressure/io; do
|
||||
printf '%s\n' "--- $file"; read_command cat "$file"
|
||||
|
||||
section "CHARGE"
|
||||
uptime
|
||||
|
||||
section "MÉMOIRE"
|
||||
free -h
|
||||
swapon --show
|
||||
|
||||
section "PRESSION"
|
||||
for file in /proc/pressure/{cpu,memory,io}; do
|
||||
printf '%s\n' "--- $file"
|
||||
cat "$file"
|
||||
done
|
||||
section DISQUE
|
||||
read_command df -h /
|
||||
# Aucun prompt : la lecture privilégiée refusée est rapportée, jamais contournée.
|
||||
read_command sudo -n btrfs filesystem usage /
|
||||
section 'TOP CPU'; top_processes -%cpu
|
||||
section 'TOP RAM'; top_processes -rss
|
||||
section 'SERVICES EN ÉCHEC'
|
||||
read_command systemctl --failed
|
||||
read_command systemctl --user --failed
|
||||
section 'SERVICES À LA DEMANDE'
|
||||
for unit in vmware-networks.service vmware-usbarbitrator.service smb.service nmb.service; do service_state system "$unit"; done
|
||||
service_state user protonmail-bridge.service
|
||||
section 'PROCESSUS OBSERVÉS (qualification manuelle)'
|
||||
if pgrep -a -f 'nextcloud|thunderbird|protonmail-bridge|/protonmail/bridge|proton-pass|ollama|llama-server'; then
|
||||
:
|
||||
else
|
||||
rc=$?
|
||||
if (( rc == 1 )); then echo 'Aucun processus correspondant observé.'
|
||||
else printf '[!] Recherche de processus échouée (%s).\n' "$rc" >&2; status=1; fi
|
||||
fi
|
||||
section BATTERIE
|
||||
if [[ -r /sys/class/power_supply/BAT1/charge_control_end_threshold ]]; then
|
||||
read_command cat /sys/class/power_supply/BAT1/charge_control_end_threshold
|
||||
else
|
||||
echo 'Seuil BAT1 indisponible ou illisible ; état non établi.'
|
||||
fi
|
||||
section DÉMARRAGE
|
||||
read_command systemd-analyze
|
||||
read_command systemd-analyze blame
|
||||
exit "$status"
|
||||
|
||||
section "DISQUE"
|
||||
df -h /
|
||||
sudo btrfs filesystem usage /
|
||||
|
||||
section "TOP CPU"
|
||||
ps -eo pid,comm,%cpu,%mem,rss --sort=-%cpu | head -n 20
|
||||
|
||||
section "TOP RAM"
|
||||
ps -eo pid,comm,%cpu,%mem,rss --sort=-rss | head -n 20
|
||||
|
||||
section "SERVICES EN ÉCHEC"
|
||||
systemctl --failed
|
||||
systemctl --user --failed
|
||||
|
||||
section "SERVICES À LA DEMANDE"
|
||||
for unit in vmware-networks.service vmware-usbarbitrator.service smb.service nmb.service; do
|
||||
printf '%-36s enabled=%-10s active=%s\n' \
|
||||
"$unit" \
|
||||
"$(systemctl is-enabled "$unit" 2>/dev/null || true)" \
|
||||
"$(systemctl is-active "$unit" 2>/dev/null || true)"
|
||||
done
|
||||
printf '%-36s enabled=%-10s active=%s\n' \
|
||||
protonmail-bridge.service \
|
||||
"$(systemctl --user is-enabled protonmail-bridge.service 2>/dev/null || true)" \
|
||||
"$(systemctl --user is-active protonmail-bridge.service 2>/dev/null || true)"
|
||||
|
||||
section "PROCESSUS INDÉSIRABLES AU REPOS"
|
||||
pgrep -a -f 'nextcloud|thunderbird|protonmail-bridge|/protonmail/bridge|proton-pass|ollama|llama-server' \
|
||||
|| echo "Aucun"
|
||||
|
||||
section "BATTERIE"
|
||||
cat /sys/class/power_supply/BAT1/charge_control_end_threshold 2>/dev/null || true
|
||||
|
||||
section "DÉMARRAGE"
|
||||
systemd-analyze
|
||||
systemd-analyze blame | head -n 15
|
||||
|
|
|
|||
|
|
@ -1,416 +1,234 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
IFS=$'\n\t'
|
||||
|
||||
readonly REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
# Contrôle privilégié fixe : sudo et find doivent réussir avant qu'un état
|
||||
# soit utilisable. La cible est une donnée positionnelle, jamais du code.
|
||||
readonly INSPECT_CODE='
|
||||
target="$1"
|
||||
[[ "$target" == /* && "$target" != / ]] || exit 2
|
||||
rest="${target#/}"
|
||||
cursor=/
|
||||
while [[ -n "$rest" ]]; do
|
||||
name="${rest%%/*}"
|
||||
[[ -n "$name" && "$name" != . && "$name" != .. ]] || exit 2
|
||||
if [[ "$rest" == */* ]]; then rest="${rest#*/}"; last=0
|
||||
else rest=""; last=1; fi
|
||||
kind="$(/usr/bin/find "$cursor" -mindepth 1 -maxdepth 1 -name "$name" -printf "%y\n" -quit)" || exit 3
|
||||
case "$kind" in
|
||||
"") printf "ABSENT\n"; exit 0 ;;
|
||||
l) printf "LINK\n"; exit 0 ;;
|
||||
d) if (( last )); then printf "INVALID\n"; exit 0; fi ;;
|
||||
f) if (( last )); then printf "EXISTING\n"; else printf "INVALID\n"; fi; exit 0 ;;
|
||||
*) printf "INVALID\n"; exit 0 ;;
|
||||
esac
|
||||
if [[ "$cursor" == / ]]; then cursor="/$name"; else cursor="$cursor/$name"; fi
|
||||
done
|
||||
exit 2
|
||||
'
|
||||
readonly REPO_DIR="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
)"
|
||||
|
||||
DRY_RUN=0 EXPLICIT_PARENT='' PARENT='' STAGE='' FINAL='' REVISION=''
|
||||
STAGE_ID='' FINAL_ACQUIRED=0 FINAL_ANNOUNCED=0
|
||||
STARTED='' CURRENT='' WARNINGS=0
|
||||
declare -a RESULTS=() HASHES=()
|
||||
log() {
|
||||
printf '[*] %s\n' "$*"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<'HELP'
|
||||
Usage : backup.sh [--output-dir REPERTOIRE_PARENT]
|
||||
backup.sh --dry-run [--output-dir REPERTOIRE_PARENT]
|
||||
backup.sh --help
|
||||
Export système privé pour revue ; ni sauvegarde des données personnelles,
|
||||
ni actualisation de la référence Git. --output-dir désigne un parent : chaque
|
||||
collecte crée un sous-répertoire distinct. Codes : 0 = candidat finalisé ou
|
||||
simulation ; 1 = contrôle/collecte/finalisation échoué ; 2 = usage invalide ;
|
||||
130/143 = interruption INT/TERM.
|
||||
HELP
|
||||
warn() {
|
||||
printf '[!] %s\n' "$*" >&2
|
||||
}
|
||||
fail() { printf '[!] Échec : %s\n' "$1" >&2; exit 1; }
|
||||
interrupt() {
|
||||
local signal="$1" code="$2"
|
||||
trap - INT TERM
|
||||
printf '[!] Interruption %s (%s) ; collecteur : %s\n' "$signal" "$code" "${CURRENT:-aucun}" >&2
|
||||
reconcile_finalization
|
||||
if (( FINAL_ACQUIRED )); then
|
||||
printf '[!] Finalisation acquise, terminaison interrompue ; candidat conservé : %s\n' "$FINAL" >&2
|
||||
FINAL_ANNOUNCED=1
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
# Le nom final seul ne suffit pas : une collision peut déjà l'occuper.
|
||||
# Le couple périphérique/inode identifie le répertoire privé créé par mktemp.
|
||||
reconcile_finalization() {
|
||||
local current_id
|
||||
(( FINAL_ACQUIRED == 0 )) || return 0
|
||||
[[ -n "$FINAL" && -n "$STAGE_ID" && ! -e "$STAGE" && ! -L "$FINAL" && -d "$FINAL" ]] || return 0
|
||||
current_id="$(stat -c '%d:%i' -- "$FINAL")" || return 0
|
||||
[[ "$current_id" == "$STAGE_ID" ]] && FINAL_ACQUIRED=1
|
||||
return 0
|
||||
}
|
||||
write_manifest() {
|
||||
local status="$1" ended
|
||||
[[ -n "$STAGE" && -d "$STAGE" ]] || return 0
|
||||
ended="$(date -u +%Y-%m-%dT%H:%M:%SZ)" || return 1
|
||||
{
|
||||
printf 'format\t1\nstarted_utc\t%s\nended_utc\t%s\nsource_revision\t%s\nglobal_status\t%s\n' \
|
||||
"$STARTED" "$ended" "$REVISION" "$status"
|
||||
printf 'collector\tclass\tstatus\tdetail\n'
|
||||
if (( ${#RESULTS[@]} )); then printf '%s\n' "${RESULTS[@]}"; fi
|
||||
printf 'file\tsha256\n'
|
||||
if (( ${#HASHES[@]} )); then printf '%s\n' "${HASHES[@]}"; fi
|
||||
} > "$STAGE/.manifest.tmp" || return 1
|
||||
mv -f -- "$STAGE/.manifest.tmp" "$STAGE/manifest.tsv"
|
||||
}
|
||||
on_exit() {
|
||||
local code="$1"
|
||||
trap - EXIT
|
||||
reconcile_finalization
|
||||
if (( FINAL_ACQUIRED )); then
|
||||
if (( ! FINAL_ANNOUNCED )); then
|
||||
printf '[!] Candidat de cette invocation déjà finalisé et conservé : %s\n' "$FINAL" >&2
|
||||
fi
|
||||
elif [[ -n "$STAGE" && -d "$STAGE" ]]; then
|
||||
write_manifest INCOMPLETE || printf '[!] Manifeste incomplet non actualisé.\n' >&2
|
||||
printf '[!] Export incomplet conservé : %s ; dernier collecteur : %s\n' \
|
||||
"$STAGE" "${CURRENT:-aucun}" >&2
|
||||
elif [[ -n "$FINAL" ]]; then
|
||||
printf '[!] État de finalisation indéterminé ; inspecter : %s et %s\n' "$STAGE" "$FINAL" >&2
|
||||
fi
|
||||
exit "$code"
|
||||
}
|
||||
trap 'on_exit $?' EXIT
|
||||
trap 'interrupt INT 130' INT
|
||||
trap 'interrupt TERM 143' TERM
|
||||
|
||||
while (( $# )); do
|
||||
case "$1" in
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--output-dir)
|
||||
(( $# >= 2 )) || { usage >&2; exit 2; }
|
||||
[[ -z "$EXPLICIT_PARENT" && -n "$2" && "$2" != --* ]] || { usage >&2; exit 2; }
|
||||
EXPLICIT_PARENT="$2"; shift ;;
|
||||
*) printf '[!] Option inconnue : %s\n' "$1" >&2; usage >&2; exit 2 ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
copy_root_file() {
|
||||
local source="$1"
|
||||
local destination="$2"
|
||||
local mode="${3:-0644}"
|
||||
|
||||
# Deux normalisations protègent les alias du dépôt ; chaque composant lexical
|
||||
# existant doit aussi être un vrai répertoire, sans traversée de lien.
|
||||
choose_parent() {
|
||||
local raw base component rest cursor=/ physical
|
||||
if [[ -n "$EXPLICIT_PARENT" ]]; then raw="$EXPLICIT_PARENT"
|
||||
if sudo test -f "$source"; then
|
||||
sudo install \
|
||||
-D \
|
||||
-o "$(id -u)" \
|
||||
-g "$(id -g)" \
|
||||
-m "$mode" \
|
||||
"$source" \
|
||||
"$destination"
|
||||
else
|
||||
base="${XDG_STATE_HOME:-}"
|
||||
if [[ -n "$base" && "$base" != /* ]]; then
|
||||
printf '[!] XDG_STATE_HOME relatif ignoré ; défaut sous HOME utilisé.\n' >&2
|
||||
base=''
|
||||
fi
|
||||
if [[ -z "$base" ]]; then
|
||||
[[ "${HOME:-}" == /* ]] || fail 'HOME absolu requis pour la destination par défaut'
|
||||
base="$HOME/.local/state"
|
||||
fi
|
||||
raw="$base/arch-system/exports"
|
||||
warn "Fichier absent, ignoré : $source"
|
||||
fi
|
||||
[[ "$raw" == /* ]] || raw="$PWD/$raw"
|
||||
PARENT="$(realpath -m -s -- "$raw")" || fail 'normalisation du parent impossible'
|
||||
physical="$(realpath -m -- "$raw")" || fail 'résolution du parent impossible'
|
||||
[[ "$PARENT" != / ]] || fail 'racine interdite comme parent'
|
||||
[[ "$physical" != "$REPO_DIR" && "$physical" != "$REPO_DIR/"* ]] ||
|
||||
fail 'destination dans le dépôt interdite'
|
||||
rest="${PARENT#/}"
|
||||
while [[ -n "$rest" ]]; do
|
||||
component="${rest%%/*}"
|
||||
if [[ "$cursor" == / ]]; then cursor="/$component"; else cursor="$cursor/$component"; fi
|
||||
[[ ! -L "$cursor" ]] || fail "lien dans le parent refusé : $cursor"
|
||||
[[ ! -e "$cursor" || -d "$cursor" ]] || fail "parent non répertoire : $cursor"
|
||||
if [[ "$rest" == */* ]]; then rest="${rest#*/}"; else rest=''; fi
|
||||
done
|
||||
}
|
||||
make_parent() {
|
||||
local rest="${PARENT#/}" component cursor=/ uid mode physical
|
||||
while [[ -n "$rest" ]]; do
|
||||
component="${rest%%/*}"
|
||||
if [[ "$cursor" == / ]]; then cursor="/$component"; else cursor="$cursor/$component"; fi
|
||||
[[ ! -L "$cursor" ]] || fail "lien dans le parent refusé : $cursor"
|
||||
if [[ ! -e "$cursor" ]]; then
|
||||
mkdir -m 0700 -- "$cursor" 2>/dev/null ||
|
||||
[[ -d "$cursor" && ! -L "$cursor" ]] || fail "création du parent impossible : $cursor"
|
||||
fi
|
||||
[[ -d "$cursor" && ! -L "$cursor" ]] || fail "parent invalide : $cursor"
|
||||
if [[ "$rest" == */* ]]; then rest="${rest#*/}"; else rest=''; fi
|
||||
done
|
||||
uid="$(stat -c %u -- "$PARENT")" || fail 'propriétaire du parent indisponible'
|
||||
mode="$(stat -c %a -- "$PARENT")" || fail 'permissions du parent indisponibles'
|
||||
[[ "$uid" == "$EUID" ]] || fail 'parent final non détenu par le compte courant'
|
||||
(( (8#$mode & 077) == 0 )) || fail 'parent final non privé'
|
||||
physical="$(realpath -m -- "$PARENT")" || fail 'résolution du parent impossible'
|
||||
[[ "$physical" != "$REPO_DIR" && "$physical" != "$REPO_DIR/"* ]] ||
|
||||
fail 'destination dans le dépôt interdite'
|
||||
}
|
||||
record() {
|
||||
RESULTS+=("$(printf '%s\t%s\t%s\t%s' "$1" "$2" "$3" "$4")")
|
||||
case "$3" in SUCCESS|EMPTY) ;; *) (( WARNINGS += 1 )) ;; esac
|
||||
}
|
||||
hash_output() {
|
||||
local relative="$1" digest
|
||||
# L'entrée standard force le nom « - » dans la sortie de sha256sum : un
|
||||
# antislash dans le parent ne peut plus préfixer le champ de somme.
|
||||
if digest="$(sha256sum < "$STAGE/$relative")"; then :
|
||||
else fail "empreinte impossible : $relative"; fi
|
||||
[[ "$digest" =~ ^([0-9a-f]{64})[[:space:]][[:space:]]-$ ]] ||
|
||||
fail "réponse d'empreinte invalide : $relative"
|
||||
HASHES+=("$(printf '%s\t%s' "$relative" "${BASH_REMATCH[1]}")")
|
||||
}
|
||||
collect_command() {
|
||||
local key="$1" class="$2" relative="$3" empty_ok="$4"; shift 4
|
||||
local tmp="$STAGE/.work/$key" state=SUCCESS
|
||||
CURRENT="$key"
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
record "$key" "$class" UNAVAILABLE 'commande_absente'
|
||||
[[ "$class" == optional ]] || fail "collecteur obligatoire indisponible : $key"
|
||||
return 0
|
||||
fi
|
||||
if "$@" > "$tmp"; then
|
||||
# Les anciennes listes étaient triées ; garder cet ordre déterministe
|
||||
# dans le candidat sans écrire directement dans la référence Git.
|
||||
case "$key" in
|
||||
pacman_*|system_enabled|system_running|system_timers|user_enabled|user_running|user_timers)
|
||||
LC_ALL=C sort -u -- "$tmp" > "$tmp.sorted" || fail "tri impossible : $key"
|
||||
mv -- "$tmp.sorted" "$tmp" || fail "tri impossible : $key" ;;
|
||||
esac
|
||||
if [[ ! -s "$tmp" ]]; then
|
||||
if [[ "$empty_ok" == yes ]]; then state=EMPTY
|
||||
else
|
||||
rm -f -- "$tmp"
|
||||
record "$key" "$class" COLLECT_ERROR 'sortie_vide_inattendue'
|
||||
[[ "$class" == optional ]] || fail "sortie obligatoire vide : $key"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
mkdir -p -- "$STAGE/${relative%/*}" || fail "répertoire de sortie impossible : $key"
|
||||
mv -- "$tmp" "$STAGE/$relative" || fail "écriture de sortie impossible : $key"
|
||||
hash_output "$relative"
|
||||
record "$key" "$class" "$state" "$relative"
|
||||
else
|
||||
rm -f -- "$tmp"
|
||||
record "$key" "$class" COLLECT_ERROR 'commande_en_echec_ou_selection_vide_indeterminee'
|
||||
[[ "$class" == optional ]] || fail "collecte obligatoire échouée : $key"
|
||||
fi
|
||||
}
|
||||
copy_source() {
|
||||
local key="$1" class="$2" source="$3" relative="$4" state tmp
|
||||
CURRENT="$key"
|
||||
if state="$(sudo -n /usr/bin/bash -c "$INSPECT_CODE" -- "$source")"; then :
|
||||
else
|
||||
record "$key" "$class" CHECK_ERROR 'inspection_privilegiee_en_echec'
|
||||
[[ "$class" == optional ]] || fail "contrôle obligatoire impossible : $key"
|
||||
return 0
|
||||
fi
|
||||
case "$state" in
|
||||
EXISTING) ;;
|
||||
ABSENT)
|
||||
record "$key" "$class" ABSENT 'absence_etablie'
|
||||
[[ "$class" == optional ]] || fail "source obligatoire absente : $key"
|
||||
return 0 ;;
|
||||
*)
|
||||
record "$key" "$class" CHECK_ERROR "etat_invalide_${state:-vide}"
|
||||
[[ "$class" == optional ]] || fail "réponse de contrôle invalide : $key"
|
||||
return 0 ;;
|
||||
esac
|
||||
tmp="$STAGE/.work/$key"
|
||||
if sudo -n /usr/bin/cat -- "$source" > "$tmp"; then
|
||||
mkdir -p -- "$STAGE/${relative%/*}" || fail "répertoire de sortie impossible : $key"
|
||||
mv -- "$tmp" "$STAGE/$relative" || fail "écriture de sortie impossible : $key"
|
||||
hash_output "$relative"
|
||||
if [[ -s "$STAGE/$relative" ]]; then record "$key" "$class" SUCCESS "$relative"
|
||||
else record "$key" "$class" EMPTY "$relative"; fi
|
||||
else
|
||||
rm -f -- "$tmp"
|
||||
record "$key" "$class" COLLECT_ERROR 'lecture_privilegiee_en_echec'
|
||||
[[ "$class" == optional ]] || fail "lecture obligatoire échouée : $key"
|
||||
fi
|
||||
}
|
||||
make_limine_model() {
|
||||
local raw="$STAGE/.work/limine_raw" model="$STAGE/.work/limine_model" state token tokens unsupported=0 grep_code
|
||||
CURRENT=limine_model
|
||||
if state="$(sudo -n /usr/bin/bash -c "$INSPECT_CODE" -- /boot/limine.conf)"; then :
|
||||
else record limine_model optional CHECK_ERROR 'inspection_privilegiee_en_echec'; return 0; fi
|
||||
case "$state" in
|
||||
ABSENT) record limine_model optional ABSENT 'absence_etablie'; return 0 ;;
|
||||
EXISTING) ;;
|
||||
*) record limine_model optional CHECK_ERROR "etat_invalide_${state:-vide}"; return 0 ;;
|
||||
esac
|
||||
if ! sudo -n /usr/bin/cat -- /boot/limine.conf > "$raw"; then
|
||||
rm -f -- "$raw"
|
||||
record limine_model optional COLLECT_ERROR 'lecture_privilegiee_en_echec'; return 0
|
||||
fi
|
||||
if ! awk '/^[[:space:]]*### Auto-generated by limine-snapper-sync/ {exit} {print}' "$raw" |
|
||||
sed -E -e 's/(^|[[:space:]])root=UUID=[[:xdigit:]-]+/\1root=UUID=@ROOT_UUID@/g' \
|
||||
-e 's/(^|[[:space:]])root=PARTUUID=[[:xdigit:]-]+/\1root=PARTUUID=@ROOT_PARTUUID@/g' > "$model"; then
|
||||
rm -f -- "$raw" "$model"
|
||||
record limine_model optional COLLECT_ERROR 'transformation_en_echec'; return 0
|
||||
fi
|
||||
rm -f -- "$raw"
|
||||
if [[ ! -s "$model" ]]; then
|
||||
rm -f -- "$model"; record limine_model optional COLLECT_ERROR 'modele_vide'; return 0
|
||||
fi
|
||||
if tokens="$(grep -oE '(^|[[:space:]])root=[^[:space:]]+' "$model")"; then
|
||||
while IFS= read -r token; do
|
||||
token="${token#"${token%%[![:space:]]*}"}"
|
||||
case "$token" in
|
||||
'root=UUID=@ROOT_UUID@'|'root=PARTUUID=@ROOT_PARTUUID@') ;;
|
||||
*) unsupported=1 ;;
|
||||
esac
|
||||
done <<< "$tokens"
|
||||
else
|
||||
grep_code=$?
|
||||
if (( grep_code != 1 )); then
|
||||
rm -f -- "$model"
|
||||
record limine_model optional COLLECT_ERROR 'analyse_root_en_echec'; return 0
|
||||
fi
|
||||
unsupported=1
|
||||
fi
|
||||
mkdir -p -- "$STAGE/templates" || fail 'répertoire du modèle impossible'
|
||||
mv -- "$model" "$STAGE/templates/limine.conf" || fail 'écriture du modèle impossible'
|
||||
hash_output templates/limine.conf
|
||||
if (( unsupported )); then
|
||||
record limine_model optional PORTABILITY_REVIEW 'root_non_pris_en_charge_et_autres_champs_a_relire'
|
||||
else
|
||||
record limine_model optional PORTABILITY_REVIEW 'autres_champs_a_relire'
|
||||
fi
|
||||
}
|
||||
show_plan() {
|
||||
printf '[PLAN] Parent : %s ; chaque collecte aurait un sous-répertoire unique.\n' "$PARENT"
|
||||
cat <<'PLAN'
|
||||
[PLAN] Classe | source ou commande -> destination relative au candidat
|
||||
[PLAN] obligatoire | pacman -Qqen -> pkglist/pacman.txt
|
||||
[PLAN] obligatoire | pacman -Qqem -> pkglist/foreign.txt
|
||||
[PLAN] facultatif | pacman -Qdtq -> pkglist/orphans.txt
|
||||
[PLAN] obligatoire | systemctl système enabled -> inventory/systemd/system-enabled.txt
|
||||
[PLAN] obligatoire | systemctl système running -> inventory/systemd/system-running.txt
|
||||
[PLAN] obligatoire | systemctl système timers -> inventory/systemd/system-timers.txt
|
||||
[PLAN] facultatif | systemctl système failed -> inventory/systemd/system-failed.txt
|
||||
[PLAN] facultatif | systemctl utilisateur enabled -> inventory/systemd/user-enabled.txt
|
||||
[PLAN] facultatif | systemctl utilisateur running -> inventory/systemd/user-running.txt
|
||||
[PLAN] facultatif | systemctl utilisateur timers -> inventory/systemd/user-timers.txt
|
||||
[PLAN] facultatif | systemctl utilisateur failed -> inventory/systemd/user-failed.txt
|
||||
[PLAN] obligatoire | lsblk -> inventory/storage/lsblk.txt
|
||||
[PLAN] obligatoire | findmnt -> inventory/storage/findmnt.txt
|
||||
[PLAN] obligatoire | /etc/fstab -> inventory/storage/fstab.current
|
||||
[PLAN] obligatoire | btrfs subvolume list / -> inventory/storage/btrfs-subvolumes.txt
|
||||
[PLAN] obligatoire | btrfs filesystem usage / -> inventory/storage/btrfs-usage.txt
|
||||
[PLAN] facultatif | btrfs scrub status / -> inventory/storage/btrfs-scrub.txt
|
||||
[PLAN] obligatoire | free -h -> inventory/storage/memory.txt
|
||||
[PLAN] obligatoire | swapon --show -> inventory/storage/swap.txt
|
||||
[PLAN] facultatif | nvme smart-log /dev/nvme0 -> inventory/storage/nvme-health.txt
|
||||
[PLAN] obligatoire | /etc/pacman.conf -> rootfs/etc/pacman.conf
|
||||
[PLAN] obligatoire | /etc/makepkg.conf -> rootfs/etc/makepkg.conf
|
||||
[PLAN] obligatoire | /etc/mkinitcpio.conf -> rootfs/etc/mkinitcpio.conf
|
||||
[PLAN] facultatif | /etc/nftables.conf -> rootfs/etc/nftables.conf
|
||||
[PLAN] facultatif | /etc/limine-snapper-sync.conf -> rootfs/etc/limine-snapper-sync.conf
|
||||
[PLAN] facultatif | /etc/tlp.d/10-archasp.conf -> rootfs/etc/tlp.d/10-archasp.conf
|
||||
[PLAN] facultatif | /etc/systemd/zram-generator.conf -> rootfs/etc/systemd/zram-generator.conf
|
||||
[PLAN] facultatif | /etc/snapper/configs/root -> rootfs/etc/snapper/configs/root
|
||||
[PLAN] facultatif | /usr/local/sbin/batlimit-set -> rootfs/usr/local/sbin/batlimit-set
|
||||
[PLAN] facultatif | /usr/local/sbin/vmware-stack -> rootfs/usr/local/sbin/vmware-stack
|
||||
[PLAN] facultatif | /boot/limine.conf -> templates/limine.conf
|
||||
[PLAN] Transformations : tri des listes, retrait de la section Limine générée,
|
||||
[PLAN] remplacement des identifiants root UUID/PARTUUID pris en charge ;
|
||||
[PLAN] modèle restant à relire pour sa portabilité.
|
||||
PLAN
|
||||
printf '[PLAN] Aucun fichier du dépôt ou export existant ne sera modifié.\n'
|
||||
}
|
||||
|
||||
(( EUID != 0 )) || fail 'exécution root refusée'
|
||||
command -v realpath >/dev/null 2>&1 || fail 'commande requise absente : realpath'
|
||||
choose_parent
|
||||
if (( DRY_RUN )); then show_plan; exit 0; fi
|
||||
for tool in stat mkdir mktemp mv date sha256sum git sudo sort; do
|
||||
command -v "$tool" >/dev/null 2>&1 || fail "commande requise absente : $tool"
|
||||
done
|
||||
REVISION="$(git -C "$REPO_DIR" rev-parse HEAD)" || fail 'révision source indisponible'
|
||||
[[ "$REVISION" =~ ^[[:xdigit:]]{40}$ ]] || fail 'révision source invalide'
|
||||
make_parent
|
||||
STAGE="$(mktemp -d "$PARENT/.incomplete.XXXXXXXX")" || fail 'création de la zone privée impossible'
|
||||
STARTED="$(date -u +%Y-%m-%dT%H:%M:%SZ)" || fail 'horodatage indisponible'
|
||||
mkdir -m 0700 -- "$STAGE/.work" || fail 'zone temporaire impossible'
|
||||
write_manifest INCOMPLETE || fail 'manifeste initial impossible'
|
||||
printf '[*] Export incomplet privé : %s\n' "$STAGE"
|
||||
mkdir -p \
|
||||
"$REPO_DIR/pkglist" \
|
||||
"$REPO_DIR/inventory/systemd" \
|
||||
"$REPO_DIR/inventory/storage" \
|
||||
"$REPO_DIR/inventory/boot" \
|
||||
"$REPO_DIR/rootfs/etc/snapper/configs" \
|
||||
"$REPO_DIR/rootfs/etc/systemd" \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin"
|
||||
|
||||
log "Listes des paquets explicitement installés"
|
||||
|
||||
pacman -Qqen |
|
||||
LC_ALL=C sort -u \
|
||||
> "$REPO_DIR/pkglist/pacman.txt"
|
||||
|
||||
pacman -Qqem |
|
||||
LC_ALL=C sort -u \
|
||||
> "$REPO_DIR/pkglist/aur.txt"
|
||||
|
||||
pacman -Qdtq 2>/dev/null |
|
||||
LC_ALL=C sort -u \
|
||||
> "$REPO_DIR/pkglist/orphans.txt" ||
|
||||
true
|
||||
|
||||
log "Inventaire systemd système"
|
||||
|
||||
systemctl list-unit-files \
|
||||
--type=service \
|
||||
--state=enabled \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/system-enabled.txt"
|
||||
|
||||
systemctl --type=service \
|
||||
--state=running \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/system-running.txt"
|
||||
|
||||
systemctl list-timers \
|
||||
--all \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/system-timers.txt"
|
||||
|
||||
systemctl --failed \
|
||||
--no-legend \
|
||||
--no-pager \
|
||||
> "$REPO_DIR/inventory/systemd/system-failed.txt" ||
|
||||
true
|
||||
|
||||
log "Inventaire systemd utilisateur"
|
||||
|
||||
systemctl --user list-unit-files \
|
||||
--type=service \
|
||||
--state=enabled \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/user-enabled.txt"
|
||||
|
||||
systemctl --user --type=service \
|
||||
--state=running \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/user-running.txt"
|
||||
|
||||
systemctl --user list-timers \
|
||||
--all \
|
||||
--no-legend \
|
||||
--no-pager |
|
||||
LC_ALL=C sort \
|
||||
> "$REPO_DIR/inventory/systemd/user-timers.txt"
|
||||
|
||||
systemctl --user --failed \
|
||||
--no-legend \
|
||||
--no-pager \
|
||||
> "$REPO_DIR/inventory/systemd/user-failed.txt" ||
|
||||
true
|
||||
|
||||
log "Inventaire du stockage"
|
||||
|
||||
lsblk -e7 \
|
||||
-o NAME,MODEL,SIZE,ROTA,TRAN,FSTYPE,FSVER,LABEL,UUID,PARTUUID,MOUNTPOINTS \
|
||||
> "$REPO_DIR/inventory/storage/lsblk.txt"
|
||||
|
||||
findmnt \
|
||||
> "$REPO_DIR/inventory/storage/findmnt.txt"
|
||||
|
||||
cp /etc/fstab \
|
||||
"$REPO_DIR/inventory/storage/fstab.current"
|
||||
|
||||
sudo btrfs subvolume list / \
|
||||
> "$REPO_DIR/inventory/storage/btrfs-subvolumes.txt"
|
||||
|
||||
sudo btrfs filesystem usage / \
|
||||
> "$REPO_DIR/inventory/storage/btrfs-usage.txt"
|
||||
|
||||
sudo btrfs scrub status / \
|
||||
> "$REPO_DIR/inventory/storage/btrfs-scrub.txt" ||
|
||||
true
|
||||
|
||||
free -h \
|
||||
> "$REPO_DIR/inventory/storage/memory.txt"
|
||||
|
||||
swapon --show \
|
||||
> "$REPO_DIR/inventory/storage/swap.txt"
|
||||
|
||||
collect_command pacman_native required pkglist/pacman.txt yes pacman -Qqen
|
||||
collect_command pacman_foreign required pkglist/foreign.txt yes pacman -Qqem
|
||||
collect_command pacman_orphans optional pkglist/orphans.txt yes pacman -Qdtq
|
||||
collect_command system_enabled required inventory/systemd/system-enabled.txt yes systemctl list-unit-files --type=service --state=enabled --no-legend --no-pager
|
||||
collect_command system_running required inventory/systemd/system-running.txt yes systemctl --type=service --state=running --no-legend --no-pager
|
||||
collect_command system_timers required inventory/systemd/system-timers.txt yes systemctl list-timers --all --no-legend --no-pager
|
||||
collect_command system_failed optional inventory/systemd/system-failed.txt yes systemctl --failed --no-legend --no-pager
|
||||
collect_command user_enabled optional inventory/systemd/user-enabled.txt yes systemctl --user list-unit-files --type=service --state=enabled --no-legend --no-pager
|
||||
collect_command user_running optional inventory/systemd/user-running.txt yes systemctl --user --type=service --state=running --no-legend --no-pager
|
||||
collect_command user_timers optional inventory/systemd/user-timers.txt yes systemctl --user list-timers --all --no-legend --no-pager
|
||||
collect_command user_failed optional inventory/systemd/user-failed.txt yes systemctl --user --failed --no-legend --no-pager
|
||||
collect_command lsblk required inventory/storage/lsblk.txt no lsblk -e7 -o NAME,MODEL,SIZE,ROTA,TRAN,FSTYPE,FSVER,LABEL,UUID,PARTUUID,MOUNTPOINTS
|
||||
collect_command findmnt required inventory/storage/findmnt.txt no findmnt
|
||||
copy_source fstab required /etc/fstab inventory/storage/fstab.current
|
||||
collect_command btrfs_subvolumes required inventory/storage/btrfs-subvolumes.txt no sudo -n /usr/bin/btrfs subvolume list /
|
||||
collect_command btrfs_usage required inventory/storage/btrfs-usage.txt no sudo -n /usr/bin/btrfs filesystem usage /
|
||||
collect_command btrfs_scrub optional inventory/storage/btrfs-scrub.txt yes sudo -n /usr/bin/btrfs scrub status /
|
||||
collect_command memory required inventory/storage/memory.txt no free -h
|
||||
collect_command swap required inventory/storage/swap.txt yes swapon --show
|
||||
if command -v nvme >/dev/null 2>&1 && [[ -e /dev/nvme0 ]]; then
|
||||
collect_command nvme_health optional inventory/storage/nvme-health.txt no sudo -n /usr/bin/nvme smart-log /dev/nvme0
|
||||
else record nvme_health optional UNAVAILABLE 'nvme0_ou_outil_absent'; fi
|
||||
|
||||
copy_source pacman_conf required /etc/pacman.conf rootfs/etc/pacman.conf
|
||||
copy_source makepkg_conf required /etc/makepkg.conf rootfs/etc/makepkg.conf
|
||||
copy_source mkinitcpio_conf required /etc/mkinitcpio.conf rootfs/etc/mkinitcpio.conf
|
||||
copy_source nftables_conf optional /etc/nftables.conf rootfs/etc/nftables.conf
|
||||
copy_source limine_sync_conf optional /etc/limine-snapper-sync.conf rootfs/etc/limine-snapper-sync.conf
|
||||
copy_source tlp_conf optional /etc/tlp.d/10-archasp.conf rootfs/etc/tlp.d/10-archasp.conf
|
||||
copy_source zram_conf optional /etc/systemd/zram-generator.conf rootfs/etc/systemd/zram-generator.conf
|
||||
copy_source snapper_conf optional /etc/snapper/configs/root rootfs/etc/snapper/configs/root
|
||||
copy_source batlimit_set optional /usr/local/sbin/batlimit-set rootfs/usr/local/sbin/batlimit-set
|
||||
copy_source vmware_stack optional /usr/local/sbin/vmware-stack rootfs/usr/local/sbin/vmware-stack
|
||||
make_limine_model
|
||||
|
||||
CURRENT=finalisation
|
||||
rmdir -- "$STAGE/.work" || fail 'zone temporaire non vide'
|
||||
if (( WARNINGS )); then final_status=COMPLETE_WITH_WARNINGS; else final_status=COMPLETE; fi
|
||||
write_manifest "$final_status" || fail 'manifeste final impossible'
|
||||
FINAL="$PARENT/export-${STAGE##*.incomplete.}"
|
||||
[[ ! -e "$FINAL" && ! -L "$FINAL" ]] || fail "destination finale déjà existante : $FINAL"
|
||||
# Une réponse de mv peut arriver après son effet, voire être suivie d'un signal
|
||||
# avant l'instruction suivante. Mémoriser l'identité avant cette fenêtre.
|
||||
STAGE_ID="$(stat -c '%d:%i' -- "$STAGE")" || fail 'identité de staging indisponible'
|
||||
# mv -n interdit le remplacement ; sa sortie 0 peut aussi signifier non déplacé.
|
||||
if mv -T -n -- "$STAGE" "$FINAL"; then :
|
||||
sudo nvme smart-log /dev/nvme0 \
|
||||
> "$REPO_DIR/inventory/storage/nvme-health.txt"
|
||||
else
|
||||
reconcile_finalization
|
||||
if (( FINAL_ACQUIRED )); then fail 'renommage accompli mais commande terminée en erreur'
|
||||
else fail 'renommage final impossible'; fi
|
||||
: > "$REPO_DIR/inventory/storage/nvme-health.txt"
|
||||
fi
|
||||
reconcile_finalization
|
||||
(( FINAL_ACQUIRED )) || fail 'renommage final non effectué'
|
||||
STAGE=''
|
||||
printf '[+] Candidat privé finalisé (%s) : %s\n' "$final_status" "$FINAL"
|
||||
FINAL_ANNOUNCED=1
|
||||
|
||||
log "Sauvegarde des configurations système"
|
||||
|
||||
copy_root_file \
|
||||
/etc/pacman.conf \
|
||||
"$REPO_DIR/rootfs/etc/pacman.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/makepkg.conf \
|
||||
"$REPO_DIR/rootfs/etc/makepkg.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/mkinitcpio.conf \
|
||||
"$REPO_DIR/rootfs/etc/mkinitcpio.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/nftables.conf \
|
||||
"$REPO_DIR/rootfs/etc/nftables.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/limine-snapper-sync.conf \
|
||||
"$REPO_DIR/rootfs/etc/limine-snapper-sync.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/tlp.d/10-archasp.conf \
|
||||
"$REPO_DIR/rootfs/etc/tlp.d/10-archasp.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/systemd/zram-generator.conf \
|
||||
"$REPO_DIR/rootfs/etc/systemd/zram-generator.conf"
|
||||
|
||||
copy_root_file \
|
||||
/etc/snapper/configs/root \
|
||||
"$REPO_DIR/rootfs/etc/snapper/configs/root"
|
||||
|
||||
copy_root_file \
|
||||
/usr/local/sbin/batlimit-set \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin/batlimit-set" \
|
||||
0755
|
||||
|
||||
copy_root_file \
|
||||
/usr/local/sbin/vmware-stack \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin/vmware-stack" \
|
||||
0755
|
||||
|
||||
log "Génération du template Limine"
|
||||
|
||||
if sudo test -f /boot/limine.conf; then
|
||||
mkdir -p "$REPO_DIR/templates"
|
||||
|
||||
sudo awk '
|
||||
/^[[:space:]]*### Auto-generated by limine-snapper-sync/ {
|
||||
exit
|
||||
}
|
||||
|
||||
{
|
||||
print
|
||||
}
|
||||
' /boot/limine.conf |
|
||||
sed -E \
|
||||
's/root=UUID=[[:xdigit:]-]+/root=UUID=@ROOT_UUID@/g' \
|
||||
> "$REPO_DIR/templates/limine.conf"
|
||||
else
|
||||
warn "Fichier absent : /boot/limine.conf"
|
||||
fi
|
||||
|
||||
log "Sauvegarde terminée"
|
||||
|
||||
printf '\n'
|
||||
printf 'Pacman : %s\n' \
|
||||
"$(wc -l < "$REPO_DIR/pkglist/pacman.txt")"
|
||||
|
||||
printf 'AUR : %s\n' \
|
||||
"$(wc -l < "$REPO_DIR/pkglist/aur.txt")"
|
||||
|
||||
printf 'Orphelins : %s\n' \
|
||||
"$(wc -l < "$REPO_DIR/pkglist/orphans.txt")"
|
||||
|
||||
printf '\n'
|
||||
git -C "$REPO_DIR" status --short || true
|
||||
|
|
|
|||
|
|
@ -1,53 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Static repository references; no privilege, network or host inventory reads."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
errors = []
|
||||
def error(value): errors.append(value)
|
||||
def lines(path):
|
||||
try: return (root/path).read_text(encoding='utf-8').splitlines()
|
||||
except OSError as exc: error(f'{path}: {exc}'); return []
|
||||
for name in ('pacman','aur','orphans'):
|
||||
path = f'pkglist/{name}.txt'
|
||||
values = lines(path)
|
||||
if values != sorted(set(values)) or any(not re.fullmatch(r'[A-Za-z0-9@._+:-]+', x) for x in values):
|
||||
error(f'{path}: tri, doublon ou nom invalide')
|
||||
if set(lines('pkglist/pacman.txt')) & set(lines('pkglist/aur.txt')):
|
||||
error('paquet présent dans les deux listes')
|
||||
for name in ('system-services','system-timers','on-demand-system-services'):
|
||||
path = f'manifests/{name}.txt'
|
||||
values = lines(path)
|
||||
suffix = '.timer' if name == 'system-timers' else '.service'
|
||||
if values != sorted(set(values)) or any(not re.fullmatch(r'[A-Za-z0-9@._:-]+', x) or not x.endswith(suffix) for x in values):
|
||||
error(f'{path}: unité invalide, désordre ou doublon')
|
||||
for name,needles in {
|
||||
'templates/fstab.btrfs':('@ROOT_UUID@','@EFI_UUID@','subvol=/@/.snapshots'),
|
||||
'templates/limine.conf':('@ROOT_UUID@',),
|
||||
'templates/sudoers.d/batlimit':('@USER@',),
|
||||
'templates/sudoers.d/vmware-stack':('@USER@',),
|
||||
}.items():
|
||||
value='\n'.join(lines(name))
|
||||
for needle in needles:
|
||||
if needle not in value: error(f'{name}: {needle} absent')
|
||||
for path in [root/'README.md',*sorted((root/'docs').rglob('*.md'))]:
|
||||
try: text=path.read_text(encoding='utf-8')
|
||||
except OSError as exc: error(str(exc)); continue
|
||||
for target in re.findall(r'\]\(([^)]+)\)',text):
|
||||
if '://' in target or target.startswith('#') or target.startswith('mailto:'): continue
|
||||
local=target.split('#',1)[0].split(' ',1)[0]
|
||||
if local:
|
||||
candidate=(path.parent/local).resolve()
|
||||
if not candidate.is_relative_to(root.resolve()) or not candidate.exists():
|
||||
error(f'{path.relative_to(root)}: lien local absent ou hors dépôt {target}')
|
||||
for path in root.rglob('*'):
|
||||
if '.git' in path.parts or not path.is_file(): continue
|
||||
if path.suffix in ('.key','.pem','.p12','.sqlite','.sqlite3','.db') or path.name == '.netrc':
|
||||
error(f'fichier potentiellement sensible : {path.relative_to(root)}')
|
||||
for forbidden in ('userfs','inventory/packages','rootfs/etc/sudoers.d'):
|
||||
if (root/forbidden).exists(): error(f'répertoire interdit : {forbidden}')
|
||||
for item in errors: print('[!] '+item,file=sys.stderr)
|
||||
print(f'Contrôle des listes, manifests, modèles et liens : {len(errors)} erreur(s).')
|
||||
sys.exit(bool(errors))
|
||||
|
|
@ -1,31 +1,121 @@
|
|||
#!/usr/bin/env bash
|
||||
# Contrat : contrôle local, sans collecte, privilège ni mutation du poste.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly REPO_DIR="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
)"
|
||||
|
||||
status=0
|
||||
shopt -s nullglob
|
||||
bash_files=("$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh "$ROOT"/tests/*/*.sh "$ROOT"/rootfs/usr/local/sbin/*)
|
||||
python_files=("$ROOT"/scripts/*.py "$ROOT"/tests/*/*.py)
|
||||
(( ${#bash_files[@]} >= 10 && ${#python_files[@]} >= 2 )) || {
|
||||
echo '[!] Scripts attendus absents.' >&2; exit 1;
|
||||
|
||||
fail() {
|
||||
printf '[!] %s\n' "$*" >&2
|
||||
status=1
|
||||
}
|
||||
for file in "${bash_files[@]}"; do
|
||||
[[ -f "$file" && -r "$file" ]] && bash -n "$file" || status=1
|
||||
|
||||
echo '[*] Validation syntaxique Bash'
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
bash -n "$file" || status=1
|
||||
done < <(
|
||||
find \
|
||||
"$REPO_DIR/scripts" \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin" \
|
||||
-type f \
|
||||
-print0 2>/dev/null
|
||||
)
|
||||
|
||||
echo '[*] Validation des listes de paquets'
|
||||
|
||||
for file in \
|
||||
"$REPO_DIR/pkglist/pacman.txt" \
|
||||
"$REPO_DIR/pkglist/aur.txt" \
|
||||
"$REPO_DIR/pkglist/orphans.txt"
|
||||
do
|
||||
[[ -f "$file" ]] || {
|
||||
fail "Liste absente : $file"
|
||||
continue
|
||||
}
|
||||
|
||||
LC_ALL=C sort -cu "$file" ||
|
||||
fail "Liste non triée ou contenant des doublons : $file"
|
||||
done
|
||||
for file in "${python_files[@]}"; do
|
||||
[[ -f "$file" && -r "$file" ]] || { status=1; continue; }
|
||||
python3 - "$file" <<'PY' || status=1
|
||||
import ast,sys
|
||||
try: ast.parse(open(sys.argv[1],encoding='utf-8').read(),filename=sys.argv[1])
|
||||
except (SyntaxError,OSError) as exc: print(exc,file=sys.stderr); sys.exit(1)
|
||||
PY
|
||||
done
|
||||
python3 "$ROOT/scripts/check-repo-data.py" "$ROOT" || status=1
|
||||
git -C "$ROOT" diff --no-ext-diff --check || status=1
|
||||
git -C "$ROOT" diff --cached --check || status=1
|
||||
if (( status == 0 )); then
|
||||
echo '[+] Syntaxe et données du dépôt contrôlées ; aucun système installé ni démarré.'
|
||||
else
|
||||
echo '[!] Contrôle du dépôt échoué.' >&2
|
||||
|
||||
if [[ -f "$REPO_DIR/pkglist/pacman.txt" &&
|
||||
-f "$REPO_DIR/pkglist/aur.txt" ]]; then
|
||||
if LC_ALL=C comm -12 \
|
||||
"$REPO_DIR/pkglist/pacman.txt" \
|
||||
"$REPO_DIR/pkglist/aur.txt" |
|
||||
grep -q .
|
||||
then
|
||||
fail 'Des paquets apparaissent dans les listes Pacman et AUR.'
|
||||
fi
|
||||
fi
|
||||
|
||||
echo '[*] Recherche de fichiers sensibles'
|
||||
|
||||
while IFS= read -r file; do
|
||||
fail "Fichier potentiellement sensible : $file"
|
||||
done < <(
|
||||
find "$REPO_DIR" \
|
||||
-type f \
|
||||
\( \
|
||||
-name '*.key' \
|
||||
-o -name '*.pem' \
|
||||
-o -name '*.p12' \
|
||||
-o -name '.netrc' \
|
||||
-o -name '*.sqlite' \
|
||||
-o -name '*.sqlite3' \
|
||||
-o -name '*.db' \
|
||||
\) \
|
||||
-not -path "$REPO_DIR/.git/*"
|
||||
)
|
||||
|
||||
echo '[*] Vérification de la séparation des dépôts'
|
||||
|
||||
for forbidden in \
|
||||
"$REPO_DIR/userfs" \
|
||||
"$REPO_DIR/inventory/packages" \
|
||||
"$REPO_DIR/rootfs/etc/sudoers.d"
|
||||
do
|
||||
[[ ! -e "$forbidden" ]] ||
|
||||
fail "Répertoire interdit dans arch-system : $forbidden"
|
||||
done
|
||||
|
||||
echo '[*] Vérification des templates'
|
||||
|
||||
grep -q '@ROOT_UUID@' \
|
||||
"$REPO_DIR/templates/fstab.btrfs" ||
|
||||
fail 'Placeholder @ROOT_UUID@ absent de fstab.btrfs.'
|
||||
|
||||
grep -q '@EFI_UUID@' \
|
||||
"$REPO_DIR/templates/fstab.btrfs" ||
|
||||
fail 'Placeholder @EFI_UUID@ absent de fstab.btrfs.'
|
||||
|
||||
grep -q '@ROOT_UUID@' \
|
||||
"$REPO_DIR/templates/limine.conf" ||
|
||||
fail 'Placeholder @ROOT_UUID@ absent de limine.conf.'
|
||||
|
||||
grep -q '@USER@' \
|
||||
"$REPO_DIR/templates/sudoers.d/batlimit" ||
|
||||
fail 'Placeholder @USER@ absent du sudoers batlimit.'
|
||||
|
||||
grep -q '@USER@' \
|
||||
"$REPO_DIR/templates/sudoers.d/vmware-stack" ||
|
||||
fail 'Placeholder @USER@ absent du sudoers VMware.'
|
||||
|
||||
if git -C "$REPO_DIR" \
|
||||
rev-parse --is-inside-work-tree >/dev/null 2>&1
|
||||
then
|
||||
echo '[*] Contrôle du diff Git'
|
||||
git -C "$REPO_DIR" diff --check || status=1
|
||||
fi
|
||||
|
||||
if (( status == 0 )); then
|
||||
echo '[+] Dépôt valide.'
|
||||
else
|
||||
echo '[!] Le dépôt contient encore des erreurs.' >&2
|
||||
fi
|
||||
|
||||
exit "$status"
|
||||
|
|
|
|||
|
|
@ -2,528 +2,288 @@
|
|||
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DO_PACKAGES=0 DO_CONFIGS=0 DO_TOOLS=0 DO_SERVICES=0 DRY_RUN=0
|
||||
BACKUP_DIR='' TEMP_DIR='' BENEFICIARY='' MACHINE_NAME='' DISPLAY_USER=''
|
||||
CURRENT_STAGE='précontrôles' CURRENT_ACTION=''
|
||||
COMPLETED=() APPLIED=() OFFICIAL_PACKAGES=() FOREIGN_PACKAGES=()
|
||||
SYSTEM_SERVICES=() SYSTEM_TIMERS=() ON_DEMAND_SERVICES=()
|
||||
readonly REPO_DIR="$(
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
||||
pwd
|
||||
)"
|
||||
|
||||
readonly CONFIG_SOURCES=(
|
||||
rootfs/etc/pacman.conf rootfs/etc/makepkg.conf rootfs/etc/mkinitcpio.conf
|
||||
rootfs/etc/nftables.conf rootfs/etc/limine-snapper-sync.conf
|
||||
rootfs/etc/tlp.d/10-archasp.conf rootfs/etc/systemd/zram-generator.conf
|
||||
rootfs/etc/snapper/configs/root
|
||||
)
|
||||
readonly CONFIG_TARGETS=(
|
||||
/etc/pacman.conf /etc/makepkg.conf /etc/mkinitcpio.conf
|
||||
/etc/nftables.conf /etc/limine-snapper-sync.conf
|
||||
/etc/tlp.d/10-archasp.conf /etc/systemd/zram-generator.conf
|
||||
/etc/snapper/configs/root
|
||||
)
|
||||
readonly HELPER_NAMES=(batlimit-set vmware-stack)
|
||||
readonly SUDOERS_NAMES=(batlimit vmware-stack)
|
||||
readonly BACKUP_DIR="/root/arch-system-restore-$(date +%F-%H%M%S)"
|
||||
|
||||
# Le fragment privilégié est fixe : aucun argument n'est interprété comme du
|
||||
# code. find termine avec 0 et émet un état même si le composant est absent ;
|
||||
# toute erreur d'inspection fait échouer la commande au lieu de signifier ABSENT.
|
||||
readonly INSPECT_CODE='
|
||||
target="$1"
|
||||
[[ "$target" == /* && "$target" != / ]] || exit 2
|
||||
rest="${target#/}"
|
||||
cursor=/
|
||||
while [[ -n "$rest" ]]; do
|
||||
name="${rest%%/*}"
|
||||
[[ -n "$name" && "$name" != . && "$name" != .. ]] || exit 2
|
||||
if [[ "$rest" == */* ]]; then rest="${rest#*/}"; last=0
|
||||
else rest=""; last=1; fi
|
||||
kind="$(find "$cursor" -mindepth 1 -maxdepth 1 -name "$name" -printf "%y\n" -quit)" || exit 3
|
||||
case "$kind" in
|
||||
"") printf "ABSENT\n"; exit 0 ;;
|
||||
l) printf "LINK\n"; exit 0 ;;
|
||||
d) if (( last )); then printf "INVALID\n"; exit 0; fi ;;
|
||||
f) if (( last )); then printf "EXISTING\n"; else printf "INVALID\n"; fi; exit 0 ;;
|
||||
*) printf "INVALID\n"; exit 0 ;;
|
||||
esac
|
||||
if [[ "$cursor" == / ]]; then cursor="/$name"; else cursor="$cursor/$name"; fi
|
||||
done
|
||||
exit 2
|
||||
'
|
||||
DO_PACKAGES=0
|
||||
DO_CONFIGS=0
|
||||
DO_TOOLS=0
|
||||
DO_SERVICES=0
|
||||
|
||||
usage() {
|
||||
cat <<'HELP'
|
||||
Usage : deploy.sh [--dry-run] (--packages|--configs|--tools|--services|--all) ...
|
||||
--packages Installe les listes de paquets natifs et étrangers (yay requis)
|
||||
cat <<'EOF'
|
||||
Usage : deploy.sh [options]
|
||||
|
||||
--packages Installe les paquets officiels et AUR
|
||||
--configs Restaure les configurations système suivies
|
||||
--tools Installe les helpers root et les règles sudoers
|
||||
--services Active les services et timers système voulus
|
||||
--all Exécute les quatre étapes dans cet ordre
|
||||
--dry-run Affiche un plan statique, sans privilèges ni application
|
||||
-h, --help Affiche cette aide
|
||||
Codes : 0 = plan valide, annulation ou application terminée ; 1 = précondition/application
|
||||
échouée ; 2 = usage invalide ; 130/143 = interruption INT/TERM.
|
||||
HELP
|
||||
--all Exécute toutes les étapes
|
||||
EOF
|
||||
}
|
||||
|
||||
fatal() {
|
||||
printf '[!] Échec (%s) : %s\n' "$CURRENT_STAGE" "$1" >&2
|
||||
if (( ${#COMPLETED[@]} )); then
|
||||
printf '[!] Étapes achevées : %s\n' "${COMPLETED[*]}" >&2
|
||||
else
|
||||
printf '[!] Aucune étape achevée.\n' >&2
|
||||
fi
|
||||
if (( ${#APPLIED[@]} )); then
|
||||
printf '[!] Actions déjà appliquées : %s\n' "${APPLIED[*]}" >&2
|
||||
fi
|
||||
[[ -z "$CURRENT_ACTION" ]] ||
|
||||
printf '[!] Action échouée, effets à vérifier : %s\n' "$CURRENT_ACTION" >&2
|
||||
[[ -z "$BACKUP_DIR" ]] ||
|
||||
printf '[!] Sauvegardes et marqueurs de cibles absentes : %s\n' "$BACKUP_DIR" >&2
|
||||
printf '[!] Pas de rollback global : inspecter les changements avant reprise.\n' >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run() {
|
||||
local label="$1"
|
||||
shift
|
||||
CURRENT_ACTION="$label"
|
||||
"$@" || fatal "$label"
|
||||
CURRENT_ACTION=''
|
||||
}
|
||||
|
||||
interrupt() {
|
||||
local signal="$1" status="$2"
|
||||
trap - INT TERM
|
||||
printf '[!] Interruption %s (%s), phase : %s\n' "$signal" "$status" "$CURRENT_STAGE" >&2
|
||||
if (( ${#COMPLETED[@]} )); then
|
||||
printf '[!] Étapes achevées : %s\n' "${COMPLETED[*]}" >&2
|
||||
else
|
||||
printf '[!] Aucune étape achevée.\n' >&2
|
||||
fi
|
||||
if (( ${#APPLIED[@]} )); then
|
||||
printf '[!] Actions confirmées : %s\n' "${APPLIED[*]}" >&2
|
||||
else
|
||||
printf '[!] Aucune action confirmée.\n' >&2
|
||||
fi
|
||||
[[ -z "$CURRENT_ACTION" ]] ||
|
||||
printf '[!] Action interrompue, effets à vérifier : %s\n' "$CURRENT_ACTION" >&2
|
||||
[[ -z "$BACKUP_DIR" ]] ||
|
||||
printf '[!] Sauvegardes et marqueurs conservés : %s\n' "$BACKUP_DIR" >&2
|
||||
exit "$status"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
local status="$1"
|
||||
if [[ -n "$TEMP_DIR" ]]; then
|
||||
# Supprimer uniquement les deux fichiers créés par cette invocation.
|
||||
rm -f -- "$TEMP_DIR/batlimit" "$TEMP_DIR/vmware-stack" ||
|
||||
printf '[!] Nettoyage des sudoers temporaires incomplet : %s\n' "$TEMP_DIR" >&2
|
||||
rmdir -- "$TEMP_DIR" 2>/dev/null || true
|
||||
fi
|
||||
exit "$status"
|
||||
}
|
||||
trap 'cleanup $?' EXIT
|
||||
trap 'interrupt INT 130' INT
|
||||
trap 'interrupt TERM 143' TERM
|
||||
|
||||
while (( $# > 0 )); do
|
||||
case "$1" in
|
||||
--packages) DO_PACKAGES=1 ;;
|
||||
--configs) DO_CONFIGS=1 ;;
|
||||
--tools) DO_TOOLS=1 ;;
|
||||
--services) DO_SERVICES=1 ;;
|
||||
--all) DO_PACKAGES=1; DO_CONFIGS=1; DO_TOOLS=1; DO_SERVICES=1 ;;
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) printf 'Option inconnue : %s\n' "$1" >&2; usage; exit 2 ;;
|
||||
--packages)
|
||||
DO_PACKAGES=1
|
||||
;;
|
||||
--configs)
|
||||
DO_CONFIGS=1
|
||||
;;
|
||||
--tools)
|
||||
DO_TOOLS=1
|
||||
;;
|
||||
--services)
|
||||
DO_SERVICES=1
|
||||
;;
|
||||
--all)
|
||||
DO_PACKAGES=1
|
||||
DO_CONFIGS=1
|
||||
DO_TOOLS=1
|
||||
DO_SERVICES=1
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
printf 'Option inconnue : %s\n' "$1" >&2
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
shift
|
||||
done
|
||||
|
||||
if (( DO_PACKAGES + DO_CONFIGS + DO_TOOLS + DO_SERVICES == 0 )); then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
require_source() {
|
||||
[[ -f "$1" && -r "$1" && -s "$1" && ! -L "$1" ]] ||
|
||||
fatal "source obligatoire absente, vide, illisible ou liée : $1"
|
||||
}
|
||||
read -rp \
|
||||
"Déployer arch-system sur $(hostname) pour ${USER} ? [y/N] " \
|
||||
confirm
|
||||
|
||||
# Une liste présente et vide est volontairement vide. Une substitution de
|
||||
# processus cacherait une erreur de lecture : lire directement et vérifier.
|
||||
read_list() {
|
||||
local file="$1" kind="$2" output_name="$3" line item
|
||||
local -n output="$output_name"
|
||||
local -A seen=()
|
||||
[[ -f "$file" && -r "$file" && ! -L "$file" ]] ||
|
||||
fatal "liste/manifest absent, illisible ou lié : $file"
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
item="${line#"${line%%[![:space:]]*}"}"
|
||||
item="${item%"${item##*[![:space:]]}"}"
|
||||
[[ -z "$item" || "$item" == \#* ]] && continue
|
||||
case "$kind" in
|
||||
package)
|
||||
[[ "$item" =~ ^[a-zA-Z0-9@][a-zA-Z0-9@._+-]*$ ]] ||
|
||||
fatal "nom de paquet invalide dans $file : $item" ;;
|
||||
service)
|
||||
[[ "$item" =~ ^[a-zA-Z0-9@_.-]+\.service$ ]] ||
|
||||
fatal "unité service invalide dans $file : $item" ;;
|
||||
timer)
|
||||
[[ "$item" =~ ^[a-zA-Z0-9@_.-]+\.timer$ ]] ||
|
||||
fatal "unité timer invalide dans $file : $item" ;;
|
||||
esac
|
||||
[[ ! -v "seen[$item]" ]] || fatal "doublon dans $file : $item"
|
||||
seen["$item"]=1
|
||||
output+=("$item")
|
||||
done < "$file" || fatal "lecture échouée : $file"
|
||||
}
|
||||
|
||||
require_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fatal "commande requise absente : $1"
|
||||
}
|
||||
|
||||
read_machine_name() {
|
||||
[[ -r /etc/hostname ]] || fatal 'nom de machine indisponible : /etc/hostname illisible ou absent'
|
||||
IFS= read -r MACHINE_NAME < /etc/hostname ||
|
||||
[[ -n "$MACHINE_NAME" ]] || fatal 'lecture du nom de machine échouée'
|
||||
[[ "$MACHINE_NAME" =~ ^[a-zA-Z0-9][a-zA-Z0-9.-]*$ ]] ||
|
||||
fatal 'nom de machine invalide dans /etc/hostname'
|
||||
}
|
||||
|
||||
check_beneficiary() {
|
||||
local uid
|
||||
# Ne jamais utiliser USER/SUDO_USER comme autorité pour sudoers ou AUR.
|
||||
(( EUID != 0 )) || fatal 'compte root refusé pour cette invocation'
|
||||
BENEFICIARY="$(id -un)" || fatal 'identité du compte indisponible'
|
||||
uid="$(id -u "$BENEFICIARY")" || fatal 'compte bénéficiaire inexistant'
|
||||
[[ "$BENEFICIARY" =~ ^[a-z_][a-z0-9_-]*\$?$ && "$uid" =~ ^[0-9]+$ ]] ||
|
||||
fatal 'nom de compte bénéficiaire invalide'
|
||||
(( uid > 0 )) || fatal 'compte bénéficiaire root refusé'
|
||||
}
|
||||
|
||||
check_target_local() {
|
||||
local parent="${1%/*}"
|
||||
while [[ "$parent" != / ]]; do
|
||||
[[ ! -L "$parent" ]] || fatal "répertoire cible lié refusé : $parent"
|
||||
[[ ! -e "$parent" || -d "$parent" ]] ||
|
||||
fatal "parent non répertoire refusé : $parent"
|
||||
parent="${parent%/*}"
|
||||
[[ -n "$parent" ]] || parent=/
|
||||
done
|
||||
[[ ! -L "$1" ]] || fatal "cible liée refusée : $1"
|
||||
[[ ! -e "$1" || -f "$1" ]] || fatal "cible non régulière refusée : $1"
|
||||
}
|
||||
|
||||
check_unit() {
|
||||
systemctl cat "$1" >/dev/null 2>&1 || fatal "unité requise absente : $1"
|
||||
}
|
||||
|
||||
check_units() {
|
||||
local unit
|
||||
if (( DO_SERVICES )); then
|
||||
for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do
|
||||
check_unit "$unit"
|
||||
done
|
||||
fi
|
||||
if (( DO_TOOLS || DO_SERVICES )); then
|
||||
for unit in "${ON_DEMAND_SERVICES[@]}"; do
|
||||
check_unit "$unit"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
preflight_static() {
|
||||
local i name
|
||||
if (( DO_PACKAGES )); then
|
||||
read_list "$REPO_DIR/pkglist/pacman.txt" package OFFICIAL_PACKAGES
|
||||
read_list "$REPO_DIR/pkglist/aur.txt" package FOREIGN_PACKAGES
|
||||
require_command pacman
|
||||
if (( ${#FOREIGN_PACKAGES[@]} )); then
|
||||
require_command yay
|
||||
check_beneficiary
|
||||
fi
|
||||
fi
|
||||
if (( DO_CONFIGS )); then
|
||||
for i in "${!CONFIG_SOURCES[@]}"; do
|
||||
require_source "$REPO_DIR/${CONFIG_SOURCES[i]}"
|
||||
check_target_local "${CONFIG_TARGETS[i]}"
|
||||
done
|
||||
if (( ! DO_PACKAGES )); then require_command nft; fi
|
||||
require_command systemctl
|
||||
fi
|
||||
if (( DO_TOOLS )); then
|
||||
check_beneficiary
|
||||
for name in "${HELPER_NAMES[@]}"; do
|
||||
require_source "$REPO_DIR/rootfs/usr/local/sbin/$name"
|
||||
check_target_local "/usr/local/sbin/$name"
|
||||
done
|
||||
for name in "${SUDOERS_NAMES[@]}"; do
|
||||
require_source "$REPO_DIR/templates/sudoers.d/$name"
|
||||
grep -Fq '@USER@' "$REPO_DIR/templates/sudoers.d/$name" ||
|
||||
fatal "placeholder @USER@ absent : $name"
|
||||
check_target_local "/etc/sudoers.d/$name"
|
||||
done
|
||||
require_command visudo
|
||||
fi
|
||||
if (( DO_SERVICES )); then
|
||||
read_list "$REPO_DIR/manifests/system-services.txt" service SYSTEM_SERVICES
|
||||
read_list "$REPO_DIR/manifests/system-timers.txt" timer SYSTEM_TIMERS
|
||||
fi
|
||||
if (( DO_TOOLS || DO_SERVICES )); then
|
||||
read_list "$REPO_DIR/manifests/on-demand-system-services.txt" service ON_DEMAND_SERVICES
|
||||
require_command systemctl
|
||||
fi
|
||||
if (( ! DO_PACKAGES )); then
|
||||
check_units
|
||||
fi
|
||||
if (( ! DRY_RUN )); then
|
||||
require_command sudo
|
||||
require_command install
|
||||
require_command mktemp
|
||||
require_command find
|
||||
read_machine_name
|
||||
DISPLAY_USER="$(id -un)" || fatal 'identité du compte indisponible'
|
||||
fi
|
||||
}
|
||||
|
||||
show_plan() {
|
||||
local i name unit
|
||||
printf '[PLAN] Aucun changement appliqué ; validations privilégiées non exécutées.\n'
|
||||
if (( DO_PACKAGES )); then
|
||||
printf '[PLAN] %s -> pacman -Syu --needed (%s entrées)\n' \
|
||||
"$REPO_DIR/pkglist/pacman.txt" "${#OFFICIAL_PACKAGES[@]}"
|
||||
printf '[PLAN] %s -> yay -S --needed (%s entrées étrangères)\n' \
|
||||
"$REPO_DIR/pkglist/aur.txt" "${#FOREIGN_PACKAGES[@]}"
|
||||
fi
|
||||
if (( DO_CONFIGS )); then
|
||||
printf '[PLAN] Validation nftables privilégiée différée : %s\n' \
|
||||
"$REPO_DIR/rootfs/etc/nftables.conf"
|
||||
for i in "${!CONFIG_SOURCES[@]}"; do
|
||||
printf '[PLAN] %s -> %s\n' \
|
||||
"$REPO_DIR/${CONFIG_SOURCES[i]}" "${CONFIG_TARGETS[i]}"
|
||||
done
|
||||
printf '[PLAN] systemctl daemon-reload ; tlp start si présent.\n'
|
||||
fi
|
||||
if (( DO_TOOLS )); then
|
||||
for name in "${HELPER_NAMES[@]}"; do
|
||||
printf '[PLAN] %s -> /usr/local/sbin/%s\n' \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin/$name" "$name"
|
||||
done
|
||||
for name in "${SUDOERS_NAMES[@]}"; do
|
||||
printf '[PLAN] %s -> /etc/sudoers.d/%s (visudo différé, compte %s)\n' \
|
||||
"$REPO_DIR/templates/sudoers.d/$name" "$name" "$BENEFICIARY"
|
||||
done
|
||||
fi
|
||||
if (( DO_SERVICES )); then
|
||||
for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do
|
||||
printf '[PLAN] Activer : %s\n' "$unit"
|
||||
done
|
||||
fi
|
||||
if (( DO_TOOLS || DO_SERVICES )); then
|
||||
for unit in "${ON_DEMAND_SERVICES[@]}"; do
|
||||
printf '[PLAN] Désactiver et arrêter si nécessaire : %s\n' "$unit"
|
||||
done
|
||||
fi
|
||||
if (( DO_PACKAGES )); then
|
||||
printf '[PLAN] Unités fournies par les paquets : contrôle différé après installation.\n'
|
||||
fi
|
||||
printf '[PLAN] Plan statique valide, déploiement non effectué.\n'
|
||||
}
|
||||
|
||||
preflight_privileged() {
|
||||
local name
|
||||
run 'autorisation sudo' sudo -v
|
||||
if (( DO_CONFIGS )); then
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
run 'validation nftables' sudo nft -c -f "$REPO_DIR/rootfs/etc/nftables.conf"
|
||||
elif (( ! DO_PACKAGES )); then
|
||||
fatal 'nft absent avant la phase configurations'
|
||||
fi
|
||||
fi
|
||||
if (( DO_TOOLS )); then
|
||||
TEMP_DIR="$(mktemp -d)" || fatal 'création du répertoire temporaire sudoers'
|
||||
for name in "${SUDOERS_NAMES[@]}"; do
|
||||
sed "s/@USER@/$BENEFICIARY/g" "$REPO_DIR/templates/sudoers.d/$name" \
|
||||
> "$TEMP_DIR/$name" || fatal "rendu sudoers $name"
|
||||
run "validation sudoers $name" sudo visudo -cf "$TEMP_DIR/$name"
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
# Seul un résultat strict, produit avec un code de succès, décrit la cible.
|
||||
# Un échec sudo/find, y compris avec une sortie plausible, reste indéterminé.
|
||||
check_target_privileged() {
|
||||
local target="$1" state
|
||||
if state="$(sudo /usr/bin/bash -c "$INSPECT_CODE" -- "$target")"; then
|
||||
:
|
||||
else
|
||||
fatal "contrôle privilégié impossible : $target"
|
||||
fi
|
||||
case "$state" in
|
||||
EXISTING) return 0 ;;
|
||||
ABSENT) return 1 ;;
|
||||
LINK) fatal "cible ou parent lié refusé : $target" ;;
|
||||
INVALID) fatal "cible ou parent non régulier refusé : $target" ;;
|
||||
*) fatal "réponse de contrôle invalide pour $target" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_backup_dir() {
|
||||
[[ -n "$BACKUP_DIR" ]] && return 0
|
||||
# mktemp crée atomiquement un répertoire root privé, sans collision
|
||||
# entre deux invocations commencées dans la même seconde.
|
||||
BACKUP_DIR="$(sudo mktemp -d /root/arch-system-restore.XXXXXXXX)" ||
|
||||
fatal 'création du répertoire privé de sauvegarde'
|
||||
[[ "$BACKUP_DIR" == /root/arch-system-restore.* ]] ||
|
||||
fatal 'chemin de sauvegarde inattendu'
|
||||
}
|
||||
[[ "$confirm" =~ ^[yY]$ ]] || exit 0
|
||||
|
||||
backup_target() {
|
||||
local target="$1"
|
||||
local exists=0
|
||||
if check_target_privileged "$target"; then exists=1; fi
|
||||
ensure_backup_dir
|
||||
if (( exists )); then
|
||||
run "répertoire de sauvegarde $target" sudo mkdir -p \
|
||||
"$BACKUP_DIR/existing$(dirname "$target")"
|
||||
run "sauvegarde $target" sudo cp -a -- "$target" \
|
||||
"$BACKUP_DIR/existing$target"
|
||||
else
|
||||
# Un marqueur distinct d'une copie permet de retirer manuellement
|
||||
# une cible créée par ce déploiement sans inventer un ancien contenu.
|
||||
run "marqueur de cible absente $target" sudo install -D -m 0600 \
|
||||
/dev/null "$BACKUP_DIR/absent$target"
|
||||
|
||||
if sudo test -e "$target"; then
|
||||
sudo mkdir -p "$BACKUP_DIR$(dirname "$target")"
|
||||
sudo cp -a "$target" "$BACKUP_DIR$target"
|
||||
fi
|
||||
}
|
||||
|
||||
install_target() {
|
||||
local source="$1" target="$2" mode="$3"
|
||||
install_config() {
|
||||
local source="$1"
|
||||
local target="$2"
|
||||
local mode="${3:-0644}"
|
||||
|
||||
if [[ ! -f "$source" ]]; then
|
||||
printf '[!] Source absente : %s\n' "$source" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
backup_target "$target"
|
||||
# Réexaminer la cible après la copie et avant le remplacement ; un
|
||||
# changement concurrent ne doit pas transformer la destination en lien.
|
||||
if check_target_privileged "$target"; then :; fi
|
||||
run "installation $target" sudo install -D -m "$mode" "$source" "$target"
|
||||
APPLIED+=("installation:$target")
|
||||
|
||||
sudo install \
|
||||
-D \
|
||||
-m "$mode" \
|
||||
"$source" \
|
||||
"$target"
|
||||
}
|
||||
|
||||
unit_state() {
|
||||
local unit="$1" property="$2" result
|
||||
if [[ "$property" == enabled ]]; then
|
||||
result="$(systemctl is-enabled "$unit" 2>/dev/null)" || :
|
||||
else
|
||||
result="$(systemctl is-active "$unit" 2>/dev/null)" || :
|
||||
install_package_list() {
|
||||
local manager="$1"
|
||||
local file="$2"
|
||||
local -a packages=()
|
||||
|
||||
if [[ ! -s "$file" ]]; then
|
||||
printf '[!] Liste vide : %s\n' "$file"
|
||||
return 0
|
||||
fi
|
||||
[[ -n "$result" ]] || fatal "état systemd indéterminé : $unit"
|
||||
printf '%s' "$result"
|
||||
|
||||
mapfile -t packages < <(
|
||||
grep -Ev '^[[:space:]]*(#|$)' "$file"
|
||||
)
|
||||
|
||||
(( ${#packages[@]} > 0 )) || return 0
|
||||
|
||||
case "$manager" in
|
||||
pacman)
|
||||
sudo pacman -Syu --needed "${packages[@]}"
|
||||
;;
|
||||
|
||||
yay)
|
||||
yay -S --needed "${packages[@]}"
|
||||
;;
|
||||
|
||||
*)
|
||||
printf 'Gestionnaire inconnu : %s\n' "$manager" >&2
|
||||
return 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
disable_on_demand() {
|
||||
local unit enabled active
|
||||
for unit in "${ON_DEMAND_SERVICES[@]}"; do
|
||||
check_unit "$unit"
|
||||
enabled="$(unit_state "$unit" enabled)"
|
||||
active="$(unit_state "$unit" active)"
|
||||
if [[ "$enabled" == disabled && "$active" == inactive ]]; then
|
||||
printf '[*] Déjà désactivé/inactif : %s\n' "$unit"
|
||||
continue
|
||||
fi
|
||||
[[ "$enabled" =~ ^(enabled|enabled-runtime|disabled|masked)$ &&
|
||||
"$active" =~ ^(active|activating|inactive|failed)$ ]] ||
|
||||
fatal "état systemd non pris en charge : $unit ($enabled/$active)"
|
||||
run "désactivation $unit" sudo systemctl disable --now "$unit"
|
||||
APPLIED+=("désactivation:$unit")
|
||||
done
|
||||
}
|
||||
|
||||
enable_units() {
|
||||
enable_manifest() {
|
||||
local file="$1"
|
||||
local unit
|
||||
for unit in "${SYSTEM_SERVICES[@]}" "${SYSTEM_TIMERS[@]}"; do
|
||||
check_unit "$unit"
|
||||
run "activation $unit" sudo systemctl enable "$unit"
|
||||
APPLIED+=("activation:$unit")
|
||||
done
|
||||
|
||||
[[ -f "$file" ]] || return 0
|
||||
|
||||
while IFS= read -r unit; do
|
||||
[[ -z "$unit" || "$unit" == \#* ]] && continue
|
||||
|
||||
if systemctl cat "$unit" >/dev/null 2>&1; then
|
||||
sudo systemctl enable "$unit"
|
||||
else
|
||||
printf '[!] Unité absente : %s\n' "$unit"
|
||||
fi
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
preflight_static
|
||||
if (( DRY_RUN )); then
|
||||
show_plan
|
||||
exit 0
|
||||
fi
|
||||
disable_manifest() {
|
||||
local file="$1"
|
||||
local unit
|
||||
|
||||
read -rp "Déployer arch-system sur ${MACHINE_NAME} pour ${DISPLAY_USER} ? [y/N] " confirm ||
|
||||
fatal 'confirmation indisponible'
|
||||
[[ "$confirm" =~ ^[yY]$ ]] || { printf '[*] Déploiement annulé.\n'; exit 0; }
|
||||
[[ -f "$file" ]] || return 0
|
||||
|
||||
# Les validations privilégiées précèdent tout changement durable ; les unités
|
||||
# fournies par les paquets demandés restent nécessairement vérifiées après eux.
|
||||
preflight_privileged
|
||||
while IFS= read -r unit; do
|
||||
[[ -z "$unit" || "$unit" == \#* ]] && continue
|
||||
|
||||
sudo systemctl disable --now "$unit" 2>/dev/null || true
|
||||
done < "$file"
|
||||
}
|
||||
|
||||
if (( DO_PACKAGES )); then
|
||||
CURRENT_STAGE='paquets'
|
||||
if (( ${#OFFICIAL_PACKAGES[@]} )); then
|
||||
run 'installation des paquets natifs' sudo pacman -Syu --needed \
|
||||
"${OFFICIAL_PACKAGES[@]}"
|
||||
APPLIED+=(paquets-natifs)
|
||||
else
|
||||
printf '[*] Liste native présente mais vide.\n'
|
||||
fi
|
||||
if (( ${#FOREIGN_PACKAGES[@]} )); then
|
||||
run 'installation des paquets étrangers' yay -S --needed \
|
||||
"${FOREIGN_PACKAGES[@]}"
|
||||
APPLIED+=(paquets-étrangers)
|
||||
else
|
||||
printf '[*] Liste étrangère présente mais vide.\n'
|
||||
fi
|
||||
COMPLETED+=(paquets)
|
||||
if (( DO_CONFIGS )) && ! command -v nft >/dev/null 2>&1; then
|
||||
fatal 'nft reste absent après installation des paquets'
|
||||
fi
|
||||
if (( DO_CONFIGS )); then
|
||||
run 'validation nftables différée' sudo nft -c -f \
|
||||
"$REPO_DIR/rootfs/etc/nftables.conf"
|
||||
fi
|
||||
if (( DO_TOOLS || DO_SERVICES )); then
|
||||
CURRENT_STAGE='contrôle différé des unités'
|
||||
check_units
|
||||
echo '[*] Installation des paquets officiels'
|
||||
|
||||
install_package_list \
|
||||
pacman \
|
||||
"$REPO_DIR/pkglist/pacman.txt"
|
||||
|
||||
if [[ -s "$REPO_DIR/pkglist/aur.txt" ]]; then
|
||||
if command -v yay >/dev/null 2>&1; then
|
||||
echo '[*] Installation des paquets AUR'
|
||||
|
||||
install_package_list \
|
||||
yay \
|
||||
"$REPO_DIR/pkglist/aur.txt"
|
||||
else
|
||||
echo '[!] Yay est absent : installation AUR reportée.'
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if (( DO_CONFIGS )); then
|
||||
CURRENT_STAGE='configurations'
|
||||
for i in "${!CONFIG_SOURCES[@]}"; do
|
||||
install_target "$REPO_DIR/${CONFIG_SOURCES[i]}" "${CONFIG_TARGETS[i]}" 0644
|
||||
done
|
||||
run 'rechargement systemd' sudo systemctl daemon-reload
|
||||
APPLIED+=(rechargement-systemd)
|
||||
if command -v tlp >/dev/null 2>&1; then
|
||||
run 'démarrage TLP' sudo tlp start
|
||||
APPLIED+=(démarrage-tlp)
|
||||
echo "[*] Sauvegardes dans ${BACKUP_DIR}"
|
||||
|
||||
# Valider le pare-feu avant de remplacer le fichier actif.
|
||||
if [[ -f "$REPO_DIR/rootfs/etc/nftables.conf" ]]; then
|
||||
sudo nft -c -f "$REPO_DIR/rootfs/etc/nftables.conf"
|
||||
fi
|
||||
COMPLETED+=(configurations)
|
||||
printf '[!] fstab, Limine et Secure Boot restent à finaliser manuellement.\n'
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/pacman.conf" \
|
||||
/etc/pacman.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/makepkg.conf" \
|
||||
/etc/makepkg.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/mkinitcpio.conf" \
|
||||
/etc/mkinitcpio.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/nftables.conf" \
|
||||
/etc/nftables.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/limine-snapper-sync.conf" \
|
||||
/etc/limine-snapper-sync.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/tlp.d/10-archasp.conf" \
|
||||
/etc/tlp.d/10-archasp.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/systemd/zram-generator.conf" \
|
||||
/etc/systemd/zram-generator.conf
|
||||
|
||||
install_config \
|
||||
"$REPO_DIR/rootfs/etc/snapper/configs/root" \
|
||||
/etc/snapper/configs/root
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
|
||||
if command -v tlp >/dev/null 2>&1; then
|
||||
sudo tlp start
|
||||
fi
|
||||
|
||||
echo '[!] fstab, Limine et Secure Boot restent à finaliser manuellement.'
|
||||
fi
|
||||
|
||||
if (( DO_TOOLS )); then
|
||||
CURRENT_STAGE='outils root'
|
||||
for name in "${HELPER_NAMES[@]}"; do
|
||||
install_target "$REPO_DIR/rootfs/usr/local/sbin/$name" \
|
||||
"/usr/local/sbin/$name" 0755
|
||||
echo '[*] Installation des helpers root'
|
||||
|
||||
sudo install \
|
||||
-D \
|
||||
-m 0755 \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin/batlimit-set" \
|
||||
/usr/local/sbin/batlimit-set
|
||||
|
||||
sudo install \
|
||||
-D \
|
||||
-m 0755 \
|
||||
"$REPO_DIR/rootfs/usr/local/sbin/vmware-stack" \
|
||||
/usr/local/sbin/vmware-stack
|
||||
|
||||
echo '[*] Installation des sudoers minimaux'
|
||||
|
||||
for name in batlimit vmware-stack; do
|
||||
template="$REPO_DIR/templates/sudoers.d/$name"
|
||||
temporary="$(mktemp)"
|
||||
|
||||
sed "s/@USER@/${USER}/g" \
|
||||
"$template" \
|
||||
> "$temporary"
|
||||
|
||||
sudo visudo -cf "$temporary"
|
||||
|
||||
sudo install \
|
||||
-o root \
|
||||
-g root \
|
||||
-m 0440 \
|
||||
"$temporary" \
|
||||
"/etc/sudoers.d/$name"
|
||||
|
||||
rm -f "$temporary"
|
||||
done
|
||||
for name in "${SUDOERS_NAMES[@]}"; do
|
||||
install_target "$TEMP_DIR/$name" "/etc/sudoers.d/$name" 0440
|
||||
done
|
||||
disable_on_demand
|
||||
COMPLETED+=(outils)
|
||||
|
||||
disable_manifest \
|
||||
"$REPO_DIR/manifests/on-demand-system-services.txt"
|
||||
fi
|
||||
|
||||
if (( DO_SERVICES )); then
|
||||
CURRENT_STAGE='services système'
|
||||
enable_units
|
||||
disable_on_demand
|
||||
COMPLETED+=(services)
|
||||
echo '[*] Activation des services système'
|
||||
|
||||
enable_manifest \
|
||||
"$REPO_DIR/manifests/system-services.txt"
|
||||
|
||||
enable_manifest \
|
||||
"$REPO_DIR/manifests/system-timers.txt"
|
||||
|
||||
disable_manifest \
|
||||
"$REPO_DIR/manifests/on-demand-system-services.txt"
|
||||
fi
|
||||
|
||||
printf '[+] Déploiement sélectionné terminé : %s\n' "${COMPLETED[*]}"
|
||||
[[ -z "$BACKUP_DIR" ]] || printf '[+] Sauvegardes et marqueurs : %s\n' "$BACKUP_DIR"
|
||||
printf '[!] Aucune transaction globale ni rollback automatique.\n'
|
||||
printf '[!] Vérifier fstab, Limine, mkinitcpio, Snapper, nftables et sbctl avant reboot.\n'
|
||||
echo '[+] Déploiement système terminé.'
|
||||
echo '[*] Les configurations utilisateur restent dans Dotfiles_ArchAP.'
|
||||
echo '[!] Vérifier fstab, Limine, mkinitcpio, Snapper, nftables et sbctl avant reboot.'
|
||||
|
|
|
|||
|
|
@ -1,354 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Conservative, explicit disk preparation for an inspected Arch live environment.
|
||||
|
||||
CONTRACT: no implicit target or safety bypass. JSON inspection failures forbid erasure.
|
||||
INVARIANT: every destructive command is checked before the next one runs.
|
||||
"""
|
||||
import argparse
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
MOUNT = Path('/mnt')
|
||||
SUBVOLS = ('@', '@home', '@log', '@pkg')
|
||||
OPTS = 'rw,relatime,ssd,discard=async,space_cache=v2'
|
||||
STEPS = []
|
||||
MOUNTS = []
|
||||
CURRENT = 'préconditions'
|
||||
|
||||
class Stop(Exception):
|
||||
pass
|
||||
|
||||
class Interrupted(Stop):
|
||||
def __init__(self, number):
|
||||
super().__init__(f'signal {number}')
|
||||
self.code = 128 + number
|
||||
|
||||
def command(args, *, input_text=None):
|
||||
try:
|
||||
p = subprocess.run(args, input=input_text, text=True, capture_output=True, check=False)
|
||||
except OSError as exc:
|
||||
raise Stop(f"commande indisponible {args[0]} : {exc}") from exc
|
||||
if p.returncode:
|
||||
raise Stop(f"{args[0]} a échoué (code {p.returncode}) : {p.stderr.strip()[:200]}")
|
||||
return p.stdout
|
||||
|
||||
def structured(args, key):
|
||||
try:
|
||||
data = json.loads(command(args))
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise Stop(f"réponse structurée invalide de {args[0]}") from exc
|
||||
if not isinstance(data, dict) or not isinstance(data.get(key), list):
|
||||
raise Stop(f"réponse structurée incomplète de {args[0]}")
|
||||
return data[key]
|
||||
|
||||
def disk_tree(path):
|
||||
rows = structured(['lsblk', '--json', '--tree', '--bytes', '--output',
|
||||
'PATH,TYPE,MAJ:MIN,SIZE,MODEL,SERIAL,WWN,MOUNTPOINTS,PKNAME,PARTN', path], 'blockdevices')
|
||||
if len(rows) != 1 or not isinstance(rows[0], dict):
|
||||
raise Stop('identité du disque ambiguë')
|
||||
return rows[0]
|
||||
|
||||
def descendants(node):
|
||||
if not isinstance(node, dict) or not re.fullmatch(r'\d+:\d+', str(node.get('maj:min', ''))):
|
||||
raise Stop('topologie bloc incomplète')
|
||||
yield node
|
||||
children = node.get('children', [])
|
||||
if not isinstance(children, list):
|
||||
raise Stop('descendants invalides')
|
||||
for child in children:
|
||||
yield from descendants(child)
|
||||
|
||||
def mounts():
|
||||
rows = structured(['findmnt', '--json', '--output', 'TARGET,SOURCE,MAJ:MIN,FSTYPE,OPTIONS,ID'], 'filesystems')
|
||||
def flatten(items):
|
||||
for item in items:
|
||||
if not isinstance(item, dict) or not isinstance(item.get('target'), str):
|
||||
raise Stop('montage illisible')
|
||||
yield item
|
||||
children = item.get('children', [])
|
||||
if not isinstance(children, list):
|
||||
raise Stop('arbre de montage invalide')
|
||||
yield from flatten(children)
|
||||
return list(flatten(rows))
|
||||
|
||||
def containing_mount(path, rows):
|
||||
path = os.path.realpath(path)
|
||||
matches = [m for m in rows if path == m['target'] or path.startswith(m['target'].rstrip('/') + '/')]
|
||||
if not matches:
|
||||
raise Stop(f'montage porteur inconnu : {path}')
|
||||
return max(matches, key=lambda m: len(m['target']))
|
||||
|
||||
def target_identity(path):
|
||||
# readlink -e rejects a missing path and resolves stable /dev/disk/by-id aliases.
|
||||
return command(['readlink', '-e', '--', path]).strip()
|
||||
|
||||
def device_source(value):
|
||||
# findmnt prints Btrfs subvolumes as /dev/name[/@]; the bracketed suffix
|
||||
# describes the mount, not a different block device.
|
||||
return os.path.realpath(str(value).split('[', 1)[0])
|
||||
|
||||
def mounted_subvolume(options):
|
||||
match = re.search(r'(?:^|,)subvol=/?([^,]+)(?:,|$)', str(options))
|
||||
return match.group(1) if match else None
|
||||
|
||||
def overlay_backing_safe(root_mount, all_mounts, target_ids, boot_source):
|
||||
# A visible boot medium alone is insufficient: the overlay may use another
|
||||
# disk for a lower or upper layer. Resolve every declared backing path.
|
||||
options = {}
|
||||
for field in str(root_mount.get('options', '')).split(','):
|
||||
if '=' in field:
|
||||
key, value = field.split('=', 1)
|
||||
options[key] = value
|
||||
if not options.get('lowerdir') or not options.get('upperdir') or not options.get('workdir'):
|
||||
return False
|
||||
paths = options['lowerdir'].split(':') + [options['upperdir'], options['workdir']]
|
||||
for path in paths:
|
||||
if not path.startswith('/run/archiso/') or not Path(path).exists():
|
||||
return False
|
||||
carrier = containing_mount(path, all_mounts)
|
||||
if carrier['target'] == '/' or carrier.get('maj:min') in target_ids:
|
||||
return False
|
||||
if carrier.get('fstype') == 'tmpfs':
|
||||
continue
|
||||
if carrier['target'] == '/run/archiso/bootmnt' and device_source(carrier.get('source', '')) == boot_source:
|
||||
continue
|
||||
source = device_source(carrier.get('source', ''))
|
||||
if not source.startswith('/dev/loop'):
|
||||
return False
|
||||
backing_file = Path('/sys/block') / Path(source).name / 'loop/backing_file'
|
||||
try:
|
||||
backing = backing_file.read_text(encoding='utf-8').strip()
|
||||
except OSError:
|
||||
return False
|
||||
if not backing.startswith('/run/archiso/bootmnt/'):
|
||||
return False
|
||||
if containing_mount(backing, all_mounts)['target'] != '/run/archiso/bootmnt':
|
||||
return False
|
||||
return True
|
||||
|
||||
def inspect(path, *, application):
|
||||
resolved = target_identity(path)
|
||||
if not resolved.startswith('/dev/') or '\n' in resolved:
|
||||
raise Stop('cible hors /dev ou chemin ambigu')
|
||||
tree = disk_tree(resolved)
|
||||
if tree.get('type') != 'disk' or tree.get('path') != resolved:
|
||||
raise Stop('la cible doit être un disque entier pris en charge')
|
||||
majmin = str(tree.get('maj:min', ''))
|
||||
size = tree.get('size')
|
||||
if not re.fullmatch(r'\d+:\d+', majmin) or not isinstance(size, int) or size < 2 * 1024**3:
|
||||
raise Stop('identité ou capacité du disque invalide')
|
||||
nodes = list(descendants(tree))
|
||||
ids = {n['maj:min'] for n in nodes}
|
||||
if len(ids) != len(nodes) or any(n.get('type') not in ('disk', 'part') for n in nodes):
|
||||
raise Stop('pile bloc non prise en charge')
|
||||
for n in nodes:
|
||||
if any(x is not None for x in (n.get('mountpoints') or [])):
|
||||
raise Stop('disque ou descendant monté selon lsblk')
|
||||
holders = Path('/sys/dev/block') / n['maj:min'] / 'holders'
|
||||
try:
|
||||
if any(holders.iterdir()):
|
||||
raise Stop(f'holder actif : {n["maj:min"]}')
|
||||
except OSError as exc:
|
||||
raise Stop(f'holders illisibles : {exc}') from exc
|
||||
all_mounts = mounts()
|
||||
for m in all_mounts:
|
||||
if m.get('maj:min') in ids:
|
||||
raise Stop('disque ou descendant monté selon findmnt')
|
||||
# An Arch ISO overlay is acceptable only when its physical boot medium is
|
||||
# visible as a separate block mount; all visible mounts are compared above.
|
||||
# Other overlays and multi-device Btrfs remain ambiguous and fail closed.
|
||||
boot = [m for m in all_mounts if m['target'] == '/run/archiso/bootmnt']
|
||||
live_overlay_mapped = False
|
||||
if len(boot) == 1 and str(boot[0].get('source', '')).startswith('/dev/'):
|
||||
boot_device = disk_tree(str(boot[0]['source']))
|
||||
live_overlay_mapped = (boot_device.get('type') in ('disk', 'part') and
|
||||
boot_device.get('maj:min') == boot[0].get('maj:min') and
|
||||
boot_device.get('maj:min') not in ids and
|
||||
boot_device.get('pkname') != Path(resolved).name)
|
||||
if live_overlay_mapped:
|
||||
root_mount = containing_mount('/', all_mounts)
|
||||
if root_mount.get('fstype') == 'overlay':
|
||||
live_overlay_mapped = overlay_backing_safe(root_mount, all_mounts, ids,
|
||||
device_source(boot[0]['source']))
|
||||
for protected in ('/', str(Path(__file__).resolve().parent.parent), '/run/archiso'):
|
||||
m = containing_mount(protected, all_mounts)
|
||||
if m.get('fstype') == 'overlay' and m['target'] == '/' and live_overlay_mapped:
|
||||
continue
|
||||
if m.get('fstype') in ('overlay', 'btrfs') and m.get('maj:min') not in ids:
|
||||
if application:
|
||||
raise Stop(f'stockage protégé non attribuable sans ambiguïté : {protected}')
|
||||
print(f'[!] Simulation : provenance non établie pour {protected}; application interdite sans résolution.', file=sys.stderr)
|
||||
if m.get('maj:min') in ids:
|
||||
raise Stop(f'disque utilisé par {protected}')
|
||||
swaps = command(['swapon', '--show=NAME', '--noheadings', '--raw'])
|
||||
for line in swaps.splitlines():
|
||||
if not line or not line.startswith('/'):
|
||||
raise Stop('entrée swap ambiguë')
|
||||
real = os.path.realpath(line)
|
||||
if real.startswith('/dev/'):
|
||||
root = disk_tree(real)
|
||||
if root['maj:min'] in ids:
|
||||
raise Stop('swap actif sur la cible')
|
||||
# Other block stacks are not interpreted as evidence that target is free.
|
||||
if root.get('type') not in ('disk', 'part'):
|
||||
raise Stop('swap sur pile bloc ambiguë')
|
||||
elif containing_mount(real, all_mounts).get('maj:min') in ids:
|
||||
raise Stop('swapfile actif sur la cible')
|
||||
try:
|
||||
if MOUNT.is_symlink() or not MOUNT.is_dir() or any(MOUNT.iterdir()):
|
||||
raise Stop('/mnt lié, absent ou non vide')
|
||||
except OSError as exc:
|
||||
raise Stop(f'/mnt illisible : {exc}') from exc
|
||||
if any(m['target'] == '/mnt' or m['target'].startswith('/mnt/') for m in all_mounts):
|
||||
raise Stop('/mnt ou descendant déjà monté')
|
||||
if application:
|
||||
if os.geteuid() != 0 or not Path('/sys/firmware/efi').is_dir() or not Path('/run/archiso').is_dir():
|
||||
raise Stop('application réservée au live Arch UEFI avec privilèges root')
|
||||
return resolved, majmin, size, tree
|
||||
|
||||
def step(label, args):
|
||||
global CURRENT
|
||||
CURRENT = label
|
||||
print(f'[étape] {label}: {" ".join(args)}', flush=True)
|
||||
command(args)
|
||||
STEPS.append(label)
|
||||
|
||||
def mount(label, args, path, source):
|
||||
step(label, args)
|
||||
found = [m for m in mounts() if m['target'] == path]
|
||||
if len(found) != 1 or device_source(found[0].get('source', '')) != source:
|
||||
raise Stop(f'effet du montage non établi : {path}')
|
||||
mount_id = found[0].get('id')
|
||||
if not isinstance(mount_id, int) or mount_id <= 0:
|
||||
raise Stop(f'identité du montage indisponible : {path}')
|
||||
if '-o' in args and mounted_subvolume(found[0].get('options', '')) != args[2].split('subvol=', 1)[-1]:
|
||||
raise Stop(f'option de sous-volume non établie : {path}')
|
||||
# Record only the expected mount made by this invocation; never unmount
|
||||
# an object that another actor replaced at the same path.
|
||||
MOUNTS.append((path, source, mount_id))
|
||||
|
||||
def cleanup():
|
||||
remaining = []
|
||||
for path, source, mount_id in reversed(MOUNTS):
|
||||
try:
|
||||
found = [m for m in mounts() if m['target'] == path]
|
||||
if len(found) != 1 or device_source(found[0].get('source', '')) != source or found[0].get('id') != mount_id:
|
||||
remaining.append(path)
|
||||
print(f'[!] montage à examiner, non démonté : {path}', file=sys.stderr)
|
||||
continue
|
||||
command(['umount', '--', path])
|
||||
except Stop as exc:
|
||||
remaining.append(path)
|
||||
print(f'[!] démontage ciblé échoué : {path}: {exc}', file=sys.stderr)
|
||||
return remaining
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Préparation destructive GPT/ESP/Btrfs ; application réservée au live Arch UEFI.')
|
||||
parser.add_argument('--dry-run', action='store_true', help='plan et inspections lisibles, aucun effet ni confirmation')
|
||||
parser.add_argument('disk', help='disque entier explicite, jamais une partition')
|
||||
args = parser.parse_args()
|
||||
resolved, majmin, size, tree = inspect(args.disk, application=not args.dry_run)
|
||||
print(f'Cible : {resolved} ; identité {majmin} ; taille {size} octets ; modèle {tree.get("model") or "inconnu"} ; identifiant stable {tree.get("wwn") or tree.get("serial") or "indisponible"}', flush=True)
|
||||
print('Plan : effacer signatures, GPT, ESP FAT32 1 Gio /boot, Btrfs @ @home @log @pkg @/.snapshots sous /mnt.', flush=True)
|
||||
if args.dry_run:
|
||||
print('Simulation : aucun privilège, confirmation, verrou, montage ou effacement demandé. Les propriétés non lisibles ne sont pas validées.', flush=True)
|
||||
return 0
|
||||
for tool in ('wipefs','parted','partprobe','udevadm','mkfs.fat','mkfs.btrfs','btrfs','mount','umount','mkdir'):
|
||||
if shutil.which(tool) is None:
|
||||
raise Stop(f'dépendance absente : {tool}')
|
||||
lockpath = f'/run/lock/arch-system-prepare-{majmin.replace(":", "_")}.lock'
|
||||
try:
|
||||
fd = os.open(lockpath, os.O_CREAT | os.O_RDWR | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600)
|
||||
lock = os.fdopen(fd, 'r+', encoding='ascii')
|
||||
except OSError as exc:
|
||||
raise Stop(f'verrou inaccessible : {exc}') from exc
|
||||
with lock:
|
||||
try:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError as exc:
|
||||
raise Stop('autre invocation coopérante sur cette cible') from exc
|
||||
answer = input(f'Taper exactement ERASE {resolved} {majmin} : ')
|
||||
if answer != f'ERASE {resolved} {majmin}':
|
||||
raise Stop('confirmation refusée')
|
||||
again = inspect(args.disk, application=True)
|
||||
if again[:3] != (resolved, majmin, size):
|
||||
raise Stop('identité changée avant effacement')
|
||||
try:
|
||||
step('effacement des signatures', ['wipefs','-af','--',resolved])
|
||||
step('GPT', ['parted','-s',resolved,'mklabel','gpt'])
|
||||
step('ESP', ['parted','-s',resolved,'mkpart','ESP','fat32','1MiB','1025MiB'])
|
||||
step('drapeau ESP', ['parted','-s',resolved,'set','1','esp','on'])
|
||||
step('partition Btrfs', ['parted','-s',resolved,'mkpart','ARCH','btrfs','1025MiB','100%'])
|
||||
step('partprobe', ['partprobe',resolved])
|
||||
step('udev', ['udevadm','settle'])
|
||||
rows = list(descendants(disk_tree(resolved)))
|
||||
parts = [r for r in rows if r.get('type') == 'part']
|
||||
if len(parts) != 2 or {str(p.get('partn')) for p in parts} != {'1', '2'} or any(not isinstance(p.get('path'), str) or not p['path'].startswith('/dev/') or p.get('pkname') != Path(resolved).name for p in parts):
|
||||
raise Stop('partitions attendues non établies')
|
||||
efi = next(p['path'] for p in parts if str(p['partn']) == '1')
|
||||
root = next(p['path'] for p in parts if str(p['partn']) == '2')
|
||||
if efi == root or any(p.get('maj:min') == majmin for p in parts):
|
||||
raise Stop('identité des partitions invalide')
|
||||
step('FAT32', ['mkfs.fat','-F','32','-n','EFI',efi])
|
||||
step('Btrfs', ['mkfs.btrfs','-f','-L','ARCH',root])
|
||||
mount('montage top-level', ['mount',root,str(MOUNT)], str(MOUNT), root)
|
||||
for sub in SUBVOLS:
|
||||
step('sous-volume '+sub, ['btrfs','subvolume','create',str(MOUNT / sub)])
|
||||
# Create the nested snapshot subvolume inside @, before mounting @.
|
||||
step('sous-volume @/.snapshots', ['btrfs','subvolume','create',str(MOUNT / '@' / '.snapshots')])
|
||||
step('démontage top-level', ['umount','--',str(MOUNT)])
|
||||
if any(m['target'] == str(MOUNT) for m in mounts()):
|
||||
raise Stop('démontage top-level non établi')
|
||||
MOUNTS.clear()
|
||||
mount('montage racine', ['mount','-o',OPTS+',subvol=@',root,str(MOUNT)], str(MOUNT), root)
|
||||
for rel in ('boot','home','var/log','var/cache/pacman/pkg'):
|
||||
step('point de montage '+rel, ['mkdir','-p','--',str(MOUNT / rel)])
|
||||
for rel, sub in (('home','@home'),('var/log','@log'),('var/cache/pacman/pkg','@pkg'),('.snapshots','@/.snapshots')):
|
||||
mount('montage '+rel, ['mount','-o',OPTS+',subvol='+sub,root,str(MOUNT / rel)], str(MOUNT / rel), root)
|
||||
mount('montage ESP', ['mount',efi,str(MOUNT / 'boot')], str(MOUNT / 'boot'), efi)
|
||||
final = mounts()
|
||||
expected = {str(MOUNT):root, str(MOUNT/'boot'):efi, **{str(MOUNT/rel):root for rel in ('home','var/log','var/cache/pacman/pkg','.snapshots')}}
|
||||
for path, source in expected.items():
|
||||
found = [m for m in final if m['target'] == path]
|
||||
if len(found) != 1 or device_source(found[0].get('source','')) != source:
|
||||
raise Stop('schéma final non vérifié : '+path)
|
||||
expected_subvol = {'/mnt':'@','/mnt/home':'@home','/mnt/var/log':'@log',
|
||||
'/mnt/var/cache/pacman/pkg':'@pkg','/mnt/.snapshots':'@/.snapshots'}.get(path)
|
||||
if expected_subvol and mounted_subvolume(found[0].get('options','')) != expected_subvol:
|
||||
raise Stop('sous-volume final non vérifié : '+path)
|
||||
print('[+] Disque préparé et montages vérifiés sous /mnt ; Arch non installé, démarrage non testé.', flush=True)
|
||||
return 0
|
||||
except Stop:
|
||||
raise
|
||||
|
||||
def entry():
|
||||
try:
|
||||
return main()
|
||||
except (Stop, EOFError, KeyboardInterrupt) as exc:
|
||||
print(f'[!] Arrêt : {exc or "entrée interrompue"}; étape à vérifier : {CURRENT}', file=sys.stderr)
|
||||
print('[!] Étapes accomplies : '+(', '.join(STEPS) or 'aucune'), file=sys.stderr)
|
||||
if MOUNTS:
|
||||
cleanup()
|
||||
try:
|
||||
remaining = [m['target'] for m in mounts() if m['target'] == '/mnt' or m['target'].startswith('/mnt/')]
|
||||
print('[!] Montages restants à examiner : '+(', '.join(remaining) or 'aucun'), file=sys.stderr)
|
||||
except Stop as read_error:
|
||||
print(f'[!] Montages restants non établis : {read_error}', file=sys.stderr)
|
||||
print('[!] Ne pas relancer sans inspection du disque et des effets de la dernière étape.', file=sys.stderr)
|
||||
return exc.code if isinstance(exc, Interrupted) else (130 if isinstance(exc, KeyboardInterrupt) else 1)
|
||||
|
||||
def handle_term(number, frame):
|
||||
# Keep the signal's conventional exit code while reporting any mounts
|
||||
# that still need human inspection after a partially completed operation.
|
||||
raise Interrupted(number)
|
||||
|
||||
if __name__ == '__main__':
|
||||
signal.signal(signal.SIGTERM, handle_term)
|
||||
sys.exit(entry())
|
||||
|
|
@ -1,5 +1,70 @@
|
|||
#!/usr/bin/env bash
|
||||
# WHY: keep the public entry point while the topology checks parse structured data.
|
||||
# CONTRACT: Python must be available on the live medium; no fallback to unchecked shell parsing.
|
||||
set -euo pipefail
|
||||
exec python3 "$(dirname "${BASH_SOURCE[0]}")/prepare-disk.py" "$@"
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
if (( EUID != 0 )); then
|
||||
echo "Lance ce script avec sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if (( $# != 1 )); then
|
||||
printf 'Usage : %s /dev/nvmeXnY\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
readonly DISK="$(readlink -f "$1")"
|
||||
readonly MOUNT_ROOT="/mnt"
|
||||
|
||||
[[ -b "$DISK" ]] || { echo "Périphérique invalide : $DISK" >&2; exit 1; }
|
||||
|
||||
if findmnt -rn "$MOUNT_ROOT" >/dev/null 2>&1; then
|
||||
echo "$MOUNT_ROOT est déjà monté. Démonte-le avant de continuer." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if lsblk -nr -o MOUNTPOINTS "$DISK" | grep -q '/'; then
|
||||
echo "Le disque ou une de ses partitions est monté. Abandon." >&2
|
||||
lsblk -o NAME,MODEL,SIZE,FSTYPE,MOUNTPOINTS "$DISK"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
lsblk -o NAME,MODEL,SIZE,TRAN,FSTYPE,MOUNTPOINTS "$DISK"
|
||||
printf '\nCette opération efface entièrement %s.\n' "$DISK"
|
||||
read -rp "Tape exactement ERASE $DISK : " confirmation
|
||||
[[ "$confirmation" == "ERASE $DISK" ]] || { echo "Annulé."; exit 0; }
|
||||
|
||||
part_suffix=""
|
||||
[[ "$DISK" =~ [0-9]$ ]] && part_suffix="p"
|
||||
readonly EFI_PART="${DISK}${part_suffix}1"
|
||||
readonly ROOT_PART="${DISK}${part_suffix}2"
|
||||
|
||||
wipefs -af "$DISK"
|
||||
parted -s "$DISK" mklabel gpt
|
||||
parted -s "$DISK" mkpart ESP fat32 1MiB 1025MiB
|
||||
parted -s "$DISK" set 1 esp on
|
||||
parted -s "$DISK" mkpart ARCH btrfs 1025MiB 100%
|
||||
partprobe "$DISK"
|
||||
udevadm settle
|
||||
|
||||
mkfs.fat -F 32 -n EFI "$EFI_PART"
|
||||
mkfs.btrfs -f -L ARCH "$ROOT_PART"
|
||||
|
||||
mount "$ROOT_PART" "$MOUNT_ROOT"
|
||||
btrfs subvolume create "$MOUNT_ROOT/@"
|
||||
btrfs subvolume create "$MOUNT_ROOT/@home"
|
||||
btrfs subvolume create "$MOUNT_ROOT/@log"
|
||||
btrfs subvolume create "$MOUNT_ROOT/@pkg"
|
||||
umount "$MOUNT_ROOT"
|
||||
|
||||
readonly OPTS="rw,relatime,ssd,discard=async,space_cache=v2"
|
||||
mount -o "$OPTS,subvol=@" "$ROOT_PART" "$MOUNT_ROOT"
|
||||
mkdir -p "$MOUNT_ROOT"/{boot,home,var/log,var/cache/pacman/pkg,.snapshots}
|
||||
btrfs subvolume create "$MOUNT_ROOT/.snapshots"
|
||||
mount -o "$OPTS,subvol=@home" "$ROOT_PART" "$MOUNT_ROOT/home"
|
||||
mount -o "$OPTS,subvol=@log" "$ROOT_PART" "$MOUNT_ROOT/var/log"
|
||||
mount -o "$OPTS,subvol=@pkg" "$ROOT_PART" "$MOUNT_ROOT/var/cache/pacman/pkg"
|
||||
mount -o "$OPTS,subvol=@/.snapshots" "$ROOT_PART" "$MOUNT_ROOT/.snapshots"
|
||||
mount "$EFI_PART" "$MOUNT_ROOT/boot"
|
||||
|
||||
printf '\n[+] Disque préparé et monté sous %s\n' "$MOUNT_ROOT"
|
||||
lsblk -o NAME,SIZE,FSTYPE,LABEL,MOUNTPOINTS "$DISK"
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
name="${0##*/}"
|
||||
printf '%s %s\n' "$name" "$*" >> /tmp/calls
|
||||
[[ "${FAIL:-}" != "$name" ]] || exit 47
|
||||
case "$name" in
|
||||
sudo) [[ "$*" == '-n btrfs filesystem usage /' ]] || exit 90; printf 'fixture btrfs\n' ;;
|
||||
systemctl)
|
||||
if [[ "$*" == *'is-enabled'* ]]; then
|
||||
[[ "${FAIL:-}" != service ]] || exit 47
|
||||
printf 'disabled\n'; exit 1
|
||||
elif [[ "$*" == *'is-active'* ]]; then
|
||||
[[ "${FAIL:-}" != service ]] || exit 47
|
||||
printf 'inactive\n'; exit 3
|
||||
else printf 'fixture no failed units\n'; fi ;;
|
||||
pgrep) [[ "$*" == '-a -f nextcloud|thunderbird|protonmail-bridge|/protonmail/bridge|proton-pass|ollama|llama-server' ]] || exit 90
|
||||
[[ "${FAIL:-}" != pgrep_error ]] || exit 2
|
||||
exit 1 ;;
|
||||
cat) case "$1" in /proc/pressure/*) printf 'fixture pressure\n';; /sys/class/power_supply/*) printf '60\n';; *) exit 90;; esac ;;
|
||||
uptime|free|swapon|df|ps|systemd-analyze) printf 'fixture %s\n' "$name" ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
command -v bwrap >/dev/null || { echo 'bwrap requis' >&2; exit 1; }
|
||||
BASE="$(mktemp -d)"; trap 'rm -rf -- "$BASE"' EXIT
|
||||
mkdir -p "$BASE"/{bin,tmp,proc/pressure,sys/class/power_supply/BAT1,repo/scripts}
|
||||
for p in cpu memory io; do printf 'fixture\n' > "$BASE/proc/pressure/$p"; done
|
||||
printf '60\n' > "$BASE/sys/class/power_supply/BAT1/charge_control_end_threshold"
|
||||
cp "$ROOT/scripts/audit.sh" "$BASE/repo/scripts/audit.sh"
|
||||
cp "$ROOT/tests/audit/mock-command.sh" "$BASE/mock-command.sh"
|
||||
for cmd in uptime free swapon cat df sudo ps systemctl pgrep systemd-analyze; do ln -s /opt/mock-command.sh "$BASE/bin/$cmd"; done
|
||||
ln -s /usr/bin/bash "$BASE/bin/bash"
|
||||
ln -s /usr/bin/sed "$BASE/bin/sed"
|
||||
run_case() {
|
||||
local label="$1"; shift
|
||||
: > "$BASE/tmp/calls"
|
||||
set +e
|
||||
bwrap --unshare-user --unshare-pid --unshare-net --tmpfs / \
|
||||
--ro-bind /usr /usr --symlink usr/lib /lib --symlink usr/lib /lib64 \
|
||||
--symlink usr/bin /bin --tmpfs /opt \
|
||||
--ro-bind "$BASE/repo" /opt/repo --ro-bind "$BASE/bin" /opt/bin \
|
||||
--ro-bind "$BASE/mock-command.sh" /opt/mock-command.sh \
|
||||
--bind "$BASE/tmp" /tmp --bind "$BASE/proc" /proc --bind "$BASE/sys" /sys --dev /dev \
|
||||
--setenv PATH /opt/bin --setenv FAIL "${FAIL:-}" \
|
||||
-- /opt/repo/scripts/audit.sh > "$BASE/out" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
printf '[test] %s : rc=%s\n' "$label" "$rc"
|
||||
}
|
||||
run_case nominal; [[ "$rc" == 0 ]] || { cat "$BASE/out"; exit 1; }
|
||||
grep -q 'Aucun processus correspondant observé' "$BASE/out"
|
||||
grep -q 'enabled=disabled' "$BASE/out"
|
||||
for scenario in pgrep_error service sudo ps; do
|
||||
FAIL="$scenario" run_case "$scenario"
|
||||
[[ "$rc" == 1 ]] || { cat "$BASE/out"; exit 1; }
|
||||
case "$scenario" in
|
||||
pgrep_error) grep -q 'Recherche de processus échouée' "$BASE/out"; ! grep -q 'Aucun processus correspondant observé' "$BASE/out" ;;
|
||||
service) grep -q 'ERREUR(47)' "$BASE/out" ;;
|
||||
sudo) grep -q 'Lecture échouée : sudo' "$BASE/out" ;;
|
||||
ps) grep -q 'Lecture échouée : ps' "$BASE/out" ;;
|
||||
esac
|
||||
done
|
||||
printf 'Résultat : 5 scénarios confinés de diagnostic validés.\n'
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Doublures strictes : aucune mutation ou lecture hors des fixtures /mnt.
|
||||
set -euo pipefail
|
||||
name="${0##*/}"
|
||||
printf '%s' "$name" >> /mnt/calls
|
||||
printf ' %q' "$@" >> /mnt/calls
|
||||
printf '\n' >> /mnt/calls
|
||||
|
||||
if [[ "$name" == git ]]; then
|
||||
[[ "$1" == -C && "$2" == /mnt/repo && "$3" == rev-parse && "$4" == HEAD && $# == 4 ]] ||
|
||||
{ [[ "${SIM_LEGACY:-0}" == 1 && "$1" == -C && "$2" == /mnt/repo && "$3" == status ]] || exit 90; }
|
||||
[[ "$3" == status ]] || printf '074d5dd0b2a1b362c8c0cd6413ab77f868461e25\n'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$name" == sha256sum ]]; then
|
||||
[[ $# == 0 ]] || exit 90
|
||||
case "${SIM_HASH_MODE:-}" in
|
||||
fail) exit 47 ;;
|
||||
malformed) printf 'malformed -\n'; exit 0 ;;
|
||||
'') exec /usr/bin/sha256sum ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$name" == mv ]]; then
|
||||
if [[ "${1:-}" == -T ]]; then
|
||||
[[ "$2" == -n && "$3" == -- && "$4" == /mnt/*/.incomplete.* && "$5" == /mnt/*/export-* ]] || exit 90
|
||||
if [[ "${SIM_FINAL_COLLIDE:-0}" == 1 ]]; then
|
||||
/usr/bin/mkdir -m 0700 -- "$5"
|
||||
printf 'existing candidate\n' > "$5/keep"
|
||||
fi
|
||||
if [[ -n "${SIM_FINAL_SIGNAL_PHASE:-}" ]]; then
|
||||
signal="${SIM_FINAL_SIGNAL:-TERM}"
|
||||
[[ "$signal" == INT || "$signal" == TERM ]] || exit 90
|
||||
[[ "$SIM_FINAL_SIGNAL_PHASE" == before || "$SIM_FINAL_SIGNAL_PHASE" == after ]] || exit 90
|
||||
if [[ "$SIM_FINAL_SIGNAL_PHASE" == after ]]; then /usr/bin/mv "$@" || exit 91; fi
|
||||
kill "-$signal" "$PPID"
|
||||
[[ "$signal" == INT ]] && exit 130 || exit 143
|
||||
fi
|
||||
[[ "${SIM_FINAL_FAIL:-0}" != 1 ]] || exit 55
|
||||
fi
|
||||
exec /usr/bin/mv "$@"
|
||||
fi
|
||||
if [[ "$name" == sudo ]]; then
|
||||
if [[ "${SIM_LEGACY:-0}" == 1 ]]; then
|
||||
case "${1:-}" in
|
||||
test)
|
||||
[[ "$2" == -f && ( "$3" == /etc/* || "$3" == /boot/limine.conf || "$3" == /usr/local/sbin/* ) ]] || exit 90
|
||||
[[ -f "$3" ]] ; exit ;;
|
||||
install)
|
||||
[[ "$2" == -D && "$3" == -o && "$5" == -g && "$7" == -m ]] || exit 90
|
||||
[[ "$9" == /etc/* || "$9" == /usr/local/sbin/* ]] || exit 90
|
||||
[[ "${10}" == /mnt/repo/* ]] || exit 90
|
||||
/usr/bin/install -D -m "$8" "$9" "${10}"; exit ;;
|
||||
awk)
|
||||
[[ "${@: -1}" == /boot/limine.conf ]] || exit 90
|
||||
exec /usr/bin/awk "${@:2}" ;;
|
||||
esac
|
||||
fi
|
||||
[[ "${1:-}" == -n ]] || exit 90
|
||||
shift
|
||||
case "${1:-}" in
|
||||
/usr/bin/bash)
|
||||
[[ $# == 5 && "$2" == -c && "$4" == -- ]] || exit 90
|
||||
digest="$(printf '%s' "$3" | /usr/bin/sha256sum)"
|
||||
[[ "${digest%% *}" == 9fc82ae6938b308c551562c1106372ba19194e90651a107cf819d3744ede8e4d ]] || exit 90
|
||||
target="$5"
|
||||
[[ "$target" == /etc/* || "$target" == /boot/limine.conf || "$target" == /usr/local/sbin/* ]] || exit 90
|
||||
if [[ "${SIM_INSPECT_TARGET:-}" == "$target" ]]; then
|
||||
case "${SIM_INSPECT_MODE:-}" in
|
||||
fail) exit 1 ;;
|
||||
parent_fail) exit 3 ;;
|
||||
empty) exit 0 ;;
|
||||
unknown) printf 'UNKNOWN\n'; exit 0 ;;
|
||||
valid_fail) printf 'EXISTING\n'; exit 1 ;;
|
||||
esac
|
||||
fi
|
||||
parent="${target%/*}"
|
||||
while [[ "$parent" != / ]]; do
|
||||
if [[ -L "$parent" ]]; then printf 'LINK\n'; exit 0; fi
|
||||
if [[ ! -e "$parent" ]]; then printf 'ABSENT\n'; exit 0; fi
|
||||
if [[ ! -d "$parent" ]]; then printf 'INVALID\n'; exit 0; fi
|
||||
parent="${parent%/*}"; [[ -n "$parent" ]] || parent=/
|
||||
done
|
||||
if [[ -L "$target" ]]; then printf 'LINK\n'
|
||||
elif [[ -f "$target" ]]; then printf 'EXISTING\n'
|
||||
elif [[ -e "$target" ]]; then printf 'INVALID\n'
|
||||
else printf 'ABSENT\n'; fi
|
||||
exit 0 ;;
|
||||
/usr/bin/cat)
|
||||
[[ $# == 3 && "$2" == -- ]] || exit 90
|
||||
path="$3"
|
||||
[[ "$path" == /etc/fstab || "$path" == /etc/pacman.conf ||
|
||||
"$path" == /etc/makepkg.conf || "$path" == /etc/mkinitcpio.conf ||
|
||||
"$path" == /etc/nftables.conf || "$path" == /etc/limine-snapper-sync.conf ||
|
||||
"$path" == /etc/tlp.d/10-archasp.conf ||
|
||||
"$path" == /etc/systemd/zram-generator.conf ||
|
||||
"$path" == /etc/snapper/configs/root ||
|
||||
"$path" == /usr/local/sbin/batlimit-set ||
|
||||
"$path" == /usr/local/sbin/vmware-stack ||
|
||||
"$path" == /boot/limine.conf ]] || exit 90
|
||||
if [[ "${SIM_FAIL_KEY:-}" == "read:$path" ]]; then printf 'partial\n'; exit 47; fi
|
||||
exec /usr/bin/cat -- "$path" ;;
|
||||
/usr/bin/btrfs)
|
||||
[[ "$2" == subvolume && "$3" == list && "$4" == / ||
|
||||
"$2" == filesystem && "$3" == usage && "$4" == / ||
|
||||
"$2" == scrub && "$3" == status && "$4" == / ]] || exit 90
|
||||
key="btrfs_${2}_${3}"
|
||||
if [[ "${SIM_SIGNAL_KEY:-}" == "$key" ]]; then kill "-${SIM_SIGNAL:-TERM}" "$PPID"; exit 143; fi
|
||||
if [[ "${SIM_FAIL_KEY:-}" == "$key" ]]; then printf 'partial\n'; exit 47; fi
|
||||
printf 'fixture %s\n' "$key"; exit 0 ;;
|
||||
/usr/bin/nvme)
|
||||
[[ "$2" == smart-log && "$3" == /dev/nvme0 ]] || exit 90
|
||||
printf 'fixture nvme0\n'; exit 0 ;;
|
||||
esac
|
||||
exit 90
|
||||
fi
|
||||
|
||||
case "$name" in
|
||||
pacman)
|
||||
[[ $# == 1 ]] || exit 90
|
||||
case "$1" in
|
||||
-Qqen) key=pacman_native; value=alpha ;;
|
||||
-Qqem) key=pacman_foreign; value=foreign ;;
|
||||
-Qdtq) key=pacman_orphans; value=orphan ;;
|
||||
*) exit 90 ;;
|
||||
esac ;;
|
||||
systemctl)
|
||||
if [[ "$*" == *--user* ]]; then key=user_state
|
||||
else key=system_state; fi
|
||||
value='fixture.service enabled' ;;
|
||||
lsblk)
|
||||
[[ "$1" == -e7 && "$2" == -o ]] || exit 90
|
||||
key=lsblk; value='disk fixture' ;;
|
||||
findmnt) [[ $# == 0 ]] || exit 90; key=findmnt; value='fixture mount' ;;
|
||||
free) [[ "$1" == -h ]] || exit 90; key=memory; value='fixture memory' ;;
|
||||
swapon) [[ "$1" == --show ]] || exit 90; key=swap; value='fixture swap' ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
if [[ "$key" == pacman_orphans && "${SIM_ORPHANS_EMPTY_ERROR:-0}" == 1 ]]; then exit 1; fi
|
||||
if [[ "${SIM_FAIL_KEY:-}" == "$key" ]]; then printf 'partial\n'; exit 47; fi
|
||||
if [[ "${SIM_EMPTY_KEY:-}" == "$key" ]]; then exit 0; fi
|
||||
if [[ "${SIM_SIGNAL_KEY:-}" == "$key" ]]; then kill "-${SIM_SIGNAL:-TERM}" "$PPID"; exit 143; fi
|
||||
printf '%s\n' "$value"
|
||||
|
|
@ -1,409 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Seules des copies dans bwrap lisent les sources synthétiques ; jamais ArchASP.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
command -v bwrap >/dev/null || { printf 'bwrap requis\n' >&2; exit 1; }
|
||||
TEST_HOME="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$TEST_HOME"' EXIT
|
||||
passes=0 case_dir='' rc=0
|
||||
fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; }
|
||||
pass() { passes=$((passes + 1)); }
|
||||
expect_rc() { [[ "$rc" == "$1" ]] || fail "code $rc, attendu $1 : $(cat "$case_dir/output")"; }
|
||||
contains() { grep -Fq -- "$1" "$case_dir/output" || fail "texte absent : $1"; }
|
||||
not_contains() { ! grep -Fq -- "$1" "$case_dir/output" || fail "texte inattendu : $1"; }
|
||||
no_repo_change() {
|
||||
[[ "$(cat "$case_dir/repo/pkglist/pacman.txt")" == old-list ]] || fail 'liste du dépôt modifiée'
|
||||
[[ "$(cat "$case_dir/repo/rootfs/etc/pacman.conf")" == old-config ]] || fail 'configuration du dépôt modifiée'
|
||||
[[ "$(cat "$case_dir/repo/inventory/storage/lsblk.txt")" == old-inventory ]] || fail 'inventaire du dépôt modifié'
|
||||
}
|
||||
no_complete() { [[ -z "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' -print -quit 2>/dev/null)" ]] || fail 'faux candidat complet'; }
|
||||
incomplete_marked() {
|
||||
local stage
|
||||
stage="$(find "$case_dir/exports" -maxdepth 1 -type d -name '.incomplete.*' -print -quit)"
|
||||
[[ -n "$stage" ]] || fail 'zone incomplète absente'
|
||||
grep -Fq "$(printf 'global_status\tINCOMPLETE')" "$stage/manifest.tsv" || fail 'statut incomplet absent'
|
||||
}
|
||||
one_final() {
|
||||
local n
|
||||
n="$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)"
|
||||
[[ "$n" == 1 ]] || fail "nombre de candidats complets : $n"
|
||||
final_dir="$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||||
}
|
||||
check_hashes() {
|
||||
python3 - "$1" <<'PY'
|
||||
from pathlib import Path
|
||||
import hashlib,sys,stat
|
||||
import re
|
||||
p=Path(sys.argv[1]); lines=(p/'manifest.tsv').read_text().splitlines()
|
||||
assert 'format\t1' in lines
|
||||
assert any(x.startswith('source_revision\t074d5dd0') for x in lines)
|
||||
assert any(x.startswith('global_status\tCOMPLETE') for x in lines)
|
||||
assert stat.S_IMODE(p.stat().st_mode)==0o700
|
||||
assert stat.S_IMODE((p/'manifest.tsv').stat().st_mode)==0o600
|
||||
items=lines[lines.index('file\tsha256')+1:]
|
||||
assert items
|
||||
listed=set()
|
||||
for row in items:
|
||||
rel,digest=row.split('\t')
|
||||
assert re.fullmatch(r'[0-9a-f]{64}',digest),rel
|
||||
listed.add(rel)
|
||||
f=p/rel
|
||||
assert f.is_file(),rel
|
||||
assert hashlib.sha256(f.read_bytes()).hexdigest()==digest,rel
|
||||
assert stat.S_IMODE(f.stat().st_mode)==0o600,rel
|
||||
assert 'manifest.tsv' not in [x.split('\t')[0] for x in items]
|
||||
actual={str(f.relative_to(p)) for f in p.rglob('*') if f.is_file() and f.name!='manifest.tsv'}
|
||||
assert listed==actual,(listed,actual)
|
||||
PY
|
||||
}
|
||||
|
||||
new_case() {
|
||||
case_dir="$(mktemp -d "$TEST_HOME/case.XXXXXXXX")"
|
||||
mkdir -p "$case_dir"/{repo/scripts,repo/pkglist,repo/rootfs/etc,repo/inventory/storage,etc/tlp.d,etc/systemd,etc/snapper/configs,etc/NetworkManager/system-connections,boot,usr-local-sbin,tmp,home,bin,dev,run}
|
||||
cp "$ROOT/scripts/backup.sh" "$case_dir/repo/scripts/backup.sh"
|
||||
chmod 755 "$case_dir/repo/scripts/backup.sh"
|
||||
printf 'old-list\n' > "$case_dir/repo/pkglist/pacman.txt"
|
||||
printf 'old-config\n' > "$case_dir/repo/rootfs/etc/pacman.conf"
|
||||
printf 'old-inventory\n' > "$case_dir/repo/inventory/storage/lsblk.txt"
|
||||
printf 'old-findmnt\n' > "$case_dir/repo/inventory/storage/findmnt.txt"
|
||||
for f in fstab pacman.conf makepkg.conf mkinitcpio.conf nftables.conf limine-snapper-sync.conf; do
|
||||
printf 'fixture %s\n' "$f" > "$case_dir/etc/$f"
|
||||
done
|
||||
printf 'fixture tlp\n' > "$case_dir/etc/tlp.d/10-archasp.conf"
|
||||
printf 'fixture zram\n' > "$case_dir/etc/systemd/zram-generator.conf"
|
||||
printf 'fixture snapper\n' > "$case_dir/etc/snapper/configs/root"
|
||||
printf 'fixture helper\n' > "$case_dir/usr-local-sbin/batlimit-set"
|
||||
printf 'fixture helper\n' > "$case_dir/usr-local-sbin/vmware-stack"
|
||||
cat > "$case_dir/boot/limine.conf" <<'LIMINE'
|
||||
/Arch Zen
|
||||
protocol: linux
|
||||
kernel_path: boot():/vmlinuz-linux-zen
|
||||
cmdline: root=UUID=1111-AAAA rw
|
||||
/Arch LTS
|
||||
protocol: linux
|
||||
cmdline: root=PARTUUID=2222-BBBB rw
|
||||
### Auto-generated by limine-snapper-sync
|
||||
/Snapshot private value
|
||||
LIMINE
|
||||
printf 'SECRET_NEVER_EXPORT\n' > "$case_dir/etc/NetworkManager/system-connections/secret"
|
||||
printf 'SECRET_NEVER_EXPORT\n' > "$case_dir/home/.env"
|
||||
for safe in bash cat dirname realpath stat mkdir mktemp date awk sed grep rm rmdir sort; do
|
||||
[[ -x "/usr/bin/$safe" ]] || fail "outil réel absent : /usr/bin/$safe"
|
||||
ln -s "/usr/bin/$safe" "$case_dir/bin/$safe"
|
||||
done
|
||||
for mocked in sudo pacman systemctl lsblk findmnt free swapon git mv sha256sum; do
|
||||
ln -s /mnt/mock-command.sh "$case_dir/bin/$mocked"
|
||||
done
|
||||
cp "$ROOT/tests/backup/mock-command.sh" "$case_dir/mock-command.sh"
|
||||
chmod 755 "$case_dir/mock-command.sh"
|
||||
: > "$case_dir/calls"
|
||||
}
|
||||
|
||||
# La racine hôte est en lecture seule ; /etc, /boot, /usr/local/sbin, /home,
|
||||
# /root, /run et /tmp sont remplacés ou masqués ; aucune connexion réseau.
|
||||
sandbox() {
|
||||
local uid_args=()
|
||||
[[ "${SIM_AS_ROOT:-0}" != 1 ]] || uid_args=(--uid 0 --gid 0)
|
||||
bwrap --unshare-user --unshare-pid --unshare-net "${uid_args[@]}" \
|
||||
--ro-bind / / --bind "$case_dir/etc" /etc --bind "$case_dir/boot" /boot \
|
||||
--bind "$case_dir/usr-local-sbin" /usr/local/sbin \
|
||||
--tmpfs /home --tmpfs /root --tmpfs /run \
|
||||
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt --dev /dev --proc /proc \
|
||||
--setenv PATH /mnt/bin --setenv HOME /mnt/home --setenv TMPDIR /tmp \
|
||||
--setenv XDG_STATE_HOME "${SIM_XDG:-}" \
|
||||
--setenv SIM_LEGACY "${SIM_LEGACY:-0}" \
|
||||
--setenv SIM_FAIL_KEY "${SIM_FAIL_KEY:-}" \
|
||||
--setenv SIM_EMPTY_KEY "${SIM_EMPTY_KEY:-}" \
|
||||
--setenv SIM_ORPHANS_EMPTY_ERROR "${SIM_ORPHANS_EMPTY_ERROR:-0}" \
|
||||
--setenv SIM_INSPECT_TARGET "${SIM_INSPECT_TARGET:-}" \
|
||||
--setenv SIM_INSPECT_MODE "${SIM_INSPECT_MODE:-}" \
|
||||
--setenv SIM_SIGNAL_KEY "${SIM_SIGNAL_KEY:-}" \
|
||||
--setenv SIM_SIGNAL "${SIM_SIGNAL:-TERM}" \
|
||||
--setenv SIM_FINAL_FAIL "${SIM_FINAL_FAIL:-0}" \
|
||||
--setenv SIM_FINAL_COLLIDE "${SIM_FINAL_COLLIDE:-0}" \
|
||||
--setenv SIM_FINAL_SIGNAL_PHASE "${SIM_FINAL_SIGNAL_PHASE:-}" \
|
||||
--setenv SIM_FINAL_SIGNAL "${SIM_FINAL_SIGNAL:-TERM}" \
|
||||
--setenv SIM_HASH_MODE "${SIM_HASH_MODE:-}" \
|
||||
-- /mnt/repo/scripts/backup.sh "$@"
|
||||
}
|
||||
run_case() {
|
||||
local name="$1"; shift
|
||||
set +e
|
||||
sandbox "$@" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
||||
rc=$?
|
||||
set -e
|
||||
cat "$case_dir/stdout" "$case_dir/stderr" > "$case_dir/output"
|
||||
if [[ "$name" != legacy_partial && "$name" != invalid && "$name" != missing_arg && "$name" != missing_arg_option ]]; then
|
||||
if (( rc == 0 )); then
|
||||
if [[ "$name" == dry_relative_xdg ]]; then
|
||||
[[ "$(cat "$case_dir/stderr")" == '[!] XDG_STATE_HOME relatif ignoré ; défaut sous HOME utilisé.' ]] || fail 'diagnostic XDG inattendu'
|
||||
else
|
||||
[[ ! -s "$case_dir/stderr" ]] || fail "stderr de succès inattendu : $(cat "$case_dir/stderr")"
|
||||
fi
|
||||
else
|
||||
if grep -Ev '^\[!\] ' "$case_dir/stderr" > "$case_dir/unexpected-stderr"; then
|
||||
fail "stderr d’erreur inattendu : $(cat "$case_dir/unexpected-stderr")"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
printf '[test] %s : rc=%s\n' "$name" "$rc"
|
||||
}
|
||||
|
||||
# Reproduction du script initial sur sa copie seulement. Un échec tardif
|
||||
# laisse déjà une liste remplacée alors que l'ancien inventaire demeure.
|
||||
new_case
|
||||
git -C "$ROOT" show 074d5dd0b2a1b362c8c0cd6413ab77f868461e25:scripts/backup.sh > "$case_dir/repo/scripts/backup.sh"
|
||||
SIM_LEGACY=1 SIM_FAIL_KEY=lsblk run_case legacy_partial
|
||||
expect_rc 47
|
||||
[[ "$(cat "$case_dir/repo/pkglist/pacman.txt")" == alpha ]] || fail 'défaut initial non reproduit'
|
||||
[[ "$(cat "$case_dir/repo/inventory/storage/lsblk.txt")" == partial ]] || fail 'sortie partielle initiale non reproduite'
|
||||
[[ "$(cat "$case_dir/repo/inventory/storage/findmnt.txt")" == old-findmnt ]] || fail 'mélange ancien/nouveau non reproduit'
|
||||
pass
|
||||
|
||||
new_case
|
||||
run_case help --help; expect_rc 0; contains 'Export système privé'; [[ ! -s "$case_dir/calls" ]] || fail 'appel en aide'; pass
|
||||
run_case invalid --bad; expect_rc 2; pass
|
||||
run_case missing_arg --output-dir; expect_rc 2; pass
|
||||
run_case missing_arg_option --output-dir --dry-run; expect_rc 2; pass
|
||||
SIM_AS_ROOT=1 run_case root_refused --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'exécution root refusée'; [[ ! -s "$case_dir/calls" ]] || fail 'appel en root refusé'; pass
|
||||
|
||||
new_case
|
||||
run_case dry_default --dry-run; expect_rc 0; contains '/mnt/home/.local/state/arch-system/exports'
|
||||
[[ ! -e "$case_dir/home/.local" && ! -s "$case_dir/calls" ]] || fail 'simulation avec effet'
|
||||
pass
|
||||
new_case
|
||||
SIM_XDG=/mnt/state run_case dry_xdg --dry-run; expect_rc 0; contains '/mnt/state/arch-system/exports'; pass
|
||||
new_case
|
||||
SIM_XDG=relative run_case dry_relative_xdg --dry-run; expect_rc 0
|
||||
contains 'XDG_STATE_HOME relatif ignoré'; contains '/mnt/home/.local/state/arch-system/exports'; pass
|
||||
new_case
|
||||
run_case dry_spaces --dry-run --output-dir '/mnt/parent with spaces'
|
||||
expect_rc 0; contains '/mnt/parent with spaces'; pass
|
||||
new_case
|
||||
run_case dry_normalized --dry-run --output-dir /mnt/home/../exports
|
||||
expect_rc 0; contains 'Parent : /mnt/exports'; pass
|
||||
new_case
|
||||
run_case inside_repo --dry-run --output-dir /mnt/repo/exports
|
||||
expect_rc 1; contains 'destination dans le dépôt interdite'; pass
|
||||
new_case
|
||||
ln -s /mnt/repo "$case_dir/alias"
|
||||
run_case repo_alias --dry-run --output-dir /mnt/alias/exports
|
||||
expect_rc 1; contains 'destination dans le dépôt interdite'; pass
|
||||
new_case
|
||||
ln -s /mnt/home "$case_dir/alias"
|
||||
run_case linked_parent --dry-run --output-dir /mnt/alias/exports
|
||||
expect_rc 1; contains 'lien dans le parent refusé'; pass
|
||||
|
||||
# Vérification séparée du fragment réel, sans privilèges dans le même bwrap.
|
||||
new_case
|
||||
inspect_code="$(python3 - "$ROOT/scripts/backup.sh" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
s=Path(sys.argv[1]).read_text()
|
||||
print(s.split("readonly INSPECT_CODE='",1)[1].split("\n'",1)[0])
|
||||
PY
|
||||
)"
|
||||
mkdir "$case_dir/etc/target-directory"
|
||||
ln -s /mnt/missing "$case_dir/etc/target-link"
|
||||
ln -s /mnt/missing "$case_dir/etc/parent-link"
|
||||
inspect_fragment() {
|
||||
local target="$1" expected_rc="$2" expected_state="$3" actual
|
||||
set +e
|
||||
bwrap --unshare-user --unshare-pid --unshare-net --ro-bind / / \
|
||||
--bind "$case_dir/etc" /etc --tmpfs /home --tmpfs /root --tmpfs /run \
|
||||
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt --dev /dev --proc /proc \
|
||||
--setenv PATH /mnt/bin \
|
||||
-- /usr/bin/bash -c "$inspect_code" -- "$target" > "$case_dir/inspect.out" 2> "$case_dir/inspect.err"
|
||||
actual=$?
|
||||
set -e
|
||||
[[ "$actual" == "$expected_rc" && "$(cat "$case_dir/inspect.out")" == "$expected_state" ]] ||
|
||||
fail "inspection réelle : $target = $actual/$(cat "$case_dir/inspect.out")"
|
||||
pass
|
||||
}
|
||||
inspect_fragment /etc/pacman.conf 0 EXISTING
|
||||
inspect_fragment /etc/missing 0 ABSENT
|
||||
inspect_fragment /etc/missing/child 0 ABSENT
|
||||
inspect_fragment /etc/target-directory 0 INVALID
|
||||
inspect_fragment /etc/target-link 0 LINK
|
||||
inspect_fragment /etc/parent-link/child 0 LINK
|
||||
inspect_fragment relative 2 ''
|
||||
inspect_fragment / 2 ''
|
||||
|
||||
new_case
|
||||
run_case nominal --output-dir /mnt/exports; expect_rc 0
|
||||
[[ ! -s "$case_dir/stderr" ]] || fail "stderr nominal : $(cat "$case_dir/stderr")"
|
||||
one_final; check_hashes "$final_dir"; no_repo_change
|
||||
grep -Fq 'global_status' "$final_dir/manifest.tsv" || fail 'statut global absent'
|
||||
grep -Fq 'root=UUID=@ROOT_UUID@' "$final_dir/templates/limine.conf" || fail 'UUID non remplacé'
|
||||
grep -Fq 'root=PARTUUID=@ROOT_PARTUUID@' "$final_dir/templates/limine.conf" || fail 'PARTUUID non remplacé'
|
||||
! grep -Fq 'Auto-generated' "$final_dir/templates/limine.conf" || fail 'section générée conservée'
|
||||
! grep -R -Fq 'SECRET_NEVER_EXPORT' "$final_dir" || fail 'fichier interdit collecté'
|
||||
pass
|
||||
run_case second_export --output-dir /mnt/exports; expect_rc 0
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'collision des invocations successives'
|
||||
pass
|
||||
new_case
|
||||
run_case actual_default; expect_rc 0
|
||||
default_final="$(find "$case_dir/home/.local/state/arch-system/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||||
[[ -n "$default_final" ]] || fail 'export par défaut absent'
|
||||
check_hashes "$default_final"; no_repo_change; pass
|
||||
new_case
|
||||
SIM_XDG=/mnt/custom-state run_case actual_xdg; expect_rc 0
|
||||
xdg_final="$(find "$case_dir/custom-state/arch-system/exports" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||||
[[ -n "$xdg_final" ]] || fail 'export XDG absent'
|
||||
check_hashes "$xdg_final"; pass
|
||||
new_case
|
||||
run_case actual_spaces --output-dir '/mnt/parent with spaces'; expect_rc 0
|
||||
space_final="$(find "$case_dir/parent with spaces" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||||
[[ -n "$space_final" ]] || fail 'export avec espaces absent'
|
||||
check_hashes "$space_final"; pass
|
||||
new_case
|
||||
run_case prior_for_backslash --output-dir /mnt/exports; expect_rc 0
|
||||
one_final; prior_dir="$final_dir"
|
||||
run_case actual_backslash --output-dir '/mnt/parent\backslash'; expect_rc 0
|
||||
backslash_final="$(find "$case_dir/parent\backslash" -maxdepth 1 -type d -name 'export-*' -print -quit)"
|
||||
[[ -n "$backslash_final" ]] || fail 'candidat avec antislash absent'
|
||||
check_hashes "$backslash_final"; check_hashes "$prior_dir"; no_repo_change; pass
|
||||
new_case
|
||||
mkdir -m 0755 "$case_dir/exports"
|
||||
run_case public_parent --output-dir /mnt/exports; expect_rc 1
|
||||
contains 'parent final non privé'
|
||||
[[ -z "$(find "$case_dir/exports" -mindepth 1 -print -quit)" ]] || fail 'zone créée dans parent public'
|
||||
pass
|
||||
|
||||
new_case
|
||||
set +e
|
||||
sandbox --output-dir /mnt/exports > "$case_dir/concurrent-a.out" 2> "$case_dir/concurrent-a.err" & pid_a=$!
|
||||
sandbox --output-dir /mnt/exports > "$case_dir/concurrent-b.out" 2> "$case_dir/concurrent-b.err" & pid_b=$!
|
||||
wait "$pid_a"; rc_a=$?
|
||||
wait "$pid_b"; rc_b=$?
|
||||
set -e
|
||||
[[ "$rc_a" == 0 && "$rc_b" == 0 ]] || fail 'concurrence : échec d’une invocation'
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'collision simultanée'
|
||||
for candidate in "$case_dir"/exports/export-*; do check_hashes "$candidate"; done
|
||||
no_repo_change; pass
|
||||
|
||||
new_case
|
||||
SIM_FAIL_KEY=pacman_native run_case early_failure --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'collecte obligatoire échouée : pacman_native'; no_complete; no_repo_change
|
||||
incomplete_marked
|
||||
! find "$case_dir/exports" -type f -name pacman.txt | grep -q . || fail 'sortie partielle conservée comme fichier'
|
||||
pass
|
||||
new_case
|
||||
SIM_FAIL_KEY=btrfs_filesystem_usage run_case late_failure --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'collecte obligatoire échouée : btrfs_usage'; no_complete; no_repo_change
|
||||
incomplete_marked
|
||||
pass
|
||||
new_case
|
||||
SIM_INSPECT_TARGET=/etc/fstab SIM_INSPECT_MODE=fail run_case sudo_refusal --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'contrôle obligatoire impossible : fstab'; no_complete; no_repo_change; pass
|
||||
new_case
|
||||
SIM_INSPECT_TARGET=/etc/nftables.conf SIM_INSPECT_MODE=fail run_case optional_sudo_refusal --output-dir /mnt/exports
|
||||
expect_rc 0; one_final
|
||||
grep -Fq "$(printf 'nftables_conf\toptional\tCHECK_ERROR\t')" "$final_dir/manifest.tsv" || fail 'refus facultatif assimilé à absence'
|
||||
[[ ! -e "$final_dir/rootfs/etc/nftables.conf" ]] || fail 'lecture facultative malgré refus'
|
||||
pass
|
||||
for mode in parent_fail empty unknown valid_fail; do
|
||||
new_case
|
||||
SIM_INSPECT_TARGET=/etc/fstab SIM_INSPECT_MODE="$mode" run_case "inspect_$mode" --output-dir /mnt/exports
|
||||
expect_rc 1; no_complete; no_repo_change; pass
|
||||
done
|
||||
new_case
|
||||
rm "$case_dir/etc/fstab"
|
||||
run_case missing_required --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'source obligatoire absente : fstab'; no_complete; pass
|
||||
new_case
|
||||
rm "$case_dir/etc/nftables.conf"
|
||||
SIM_EMPTY_KEY=pacman_foreign run_case empty_and_absent --output-dir /mnt/exports
|
||||
expect_rc 0; one_final; check_hashes "$final_dir"
|
||||
grep -Fq "$(printf 'pacman_foreign\trequired\tEMPTY\t')" "$final_dir/manifest.tsv" || fail 'liste vide non distinguée'
|
||||
grep -Fq "$(printf 'nftables_conf\toptional\tABSENT\t')" "$final_dir/manifest.tsv" || fail 'absence facultative non distinguée'
|
||||
grep -Fq "$(printf 'nvme_health\toptional\tUNAVAILABLE\t')" "$final_dir/manifest.tsv" || fail 'NVMe indisponible non distingué'
|
||||
pass
|
||||
new_case
|
||||
SIM_FAIL_KEY=pacman_orphans run_case optional_failure --output-dir /mnt/exports
|
||||
expect_rc 0; one_final
|
||||
grep -Fq "$(printf 'pacman_orphans\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'échec facultatif dissimulé'
|
||||
pass
|
||||
new_case
|
||||
SIM_ORPHANS_EMPTY_ERROR=1 run_case orphan_ambiguous --output-dir /mnt/exports
|
||||
expect_rc 0; one_final
|
||||
grep -Fq "$(printf 'pacman_orphans\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'code 1 d’orphelins assimilé à vide'
|
||||
[[ ! -e "$final_dir/pkglist/orphans.txt" ]] || fail 'fausse liste vide d’orphelins'
|
||||
pass
|
||||
new_case
|
||||
SIM_FAIL_KEY='read:/etc/pacman.conf' run_case privileged_read_partial --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'lecture obligatoire échouée : pacman_conf'; no_complete
|
||||
! find "$case_dir/exports" -type f -name pacman.conf | grep -q . || fail 'lecture partielle publiée'
|
||||
pass
|
||||
new_case
|
||||
SIM_FAIL_KEY=user_state run_case optional_user_failure --output-dir /mnt/exports
|
||||
expect_rc 0; one_final
|
||||
grep -Fq "$(printf 'user_enabled\toptional\tCOLLECT_ERROR\t')" "$final_dir/manifest.tsv" || fail 'échec systemd utilisateur dissimulé'
|
||||
pass
|
||||
new_case
|
||||
printf '/Arch\n cmdline: root=/dev/sda2 rw\n' > "$case_dir/boot/limine.conf"
|
||||
run_case limine_unsupported --output-dir /mnt/exports; expect_rc 0; one_final
|
||||
grep -Fq 'root_non_pris_en_charge' "$final_dir/manifest.tsv" || fail 'forme Limine non prise en charge masquée'
|
||||
pass
|
||||
new_case
|
||||
SIM_FINAL_FAIL=1 run_case finalize_fail --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'renommage final impossible'; no_complete; no_repo_change; pass
|
||||
new_case
|
||||
SIM_FINAL_COLLIDE=1 run_case finalize_collision --output-dir /mnt/exports
|
||||
expect_rc 1; contains 'renommage final non effectué'
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'collision mal traitée'
|
||||
[[ "$(cat "$case_dir"/exports/export-*/keep)" == 'existing candidate' ]] || fail 'destination préexistante écrasée'
|
||||
pass
|
||||
for hash_mode in fail malformed; do
|
||||
new_case
|
||||
run_case prior_for_hash --output-dir /mnt/exports; expect_rc 0
|
||||
one_final; prior_dir="$final_dir"
|
||||
SIM_HASH_MODE="$hash_mode" run_case "hash_$hash_mode" --output-dir /mnt/exports
|
||||
expect_rc 1
|
||||
if [[ "$hash_mode" == fail ]]; then contains 'empreinte impossible'
|
||||
else contains "réponse d'empreinte invalide"; fi
|
||||
incomplete_marked
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'faux candidat après erreur de somme'
|
||||
check_hashes "$prior_dir"; no_repo_change; pass
|
||||
done
|
||||
for phase in before after; do
|
||||
for signal in INT TERM; do
|
||||
new_case
|
||||
run_case prior_for_final_signal --output-dir /mnt/exports; expect_rc 0
|
||||
one_final; prior_dir="$final_dir"
|
||||
SIM_FINAL_SIGNAL_PHASE="$phase" SIM_FINAL_SIGNAL="$signal" run_case "final_${phase}_${signal}" --output-dir /mnt/exports
|
||||
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
|
||||
contains "Interruption $signal ("
|
||||
check_hashes "$prior_dir"; no_repo_change
|
||||
if [[ "$phase" == before ]]; then
|
||||
incomplete_marked
|
||||
contains 'Export incomplet conservé : /mnt/exports/.incomplete.'
|
||||
not_contains 'Finalisation acquise'
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'finalisation revendiquée avant renommage'
|
||||
else
|
||||
[[ -z "$(find "$case_dir/exports" -maxdepth 1 -type d -name '.incomplete.*' -print -quit)" ]] || fail 'staging restant après renommage'
|
||||
[[ "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 2 ]] || fail 'candidat finalisé non conservé'
|
||||
for candidate in "$case_dir"/exports/export-*; do
|
||||
[[ "$candidate" == "$prior_dir" ]] && continue
|
||||
check_hashes "$candidate"
|
||||
contains "Finalisation acquise, terminaison interrompue ; candidat conservé : /mnt/exports/${candidate##*/}"
|
||||
done
|
||||
fi
|
||||
pass
|
||||
done
|
||||
done
|
||||
for signal in INT TERM; do
|
||||
new_case
|
||||
run_case prior_export --output-dir /mnt/exports; expect_rc 0
|
||||
one_final; prior_dir="$final_dir"
|
||||
SIM_SIGNAL_KEY=btrfs_filesystem_usage SIM_SIGNAL="$signal" run_case "signal_$signal" --output-dir /mnt/exports
|
||||
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
|
||||
contains "Interruption $signal"
|
||||
incomplete_marked
|
||||
[[ -d "$prior_dir" && "$(find "$case_dir/exports" -maxdepth 1 -type d -name 'export-*' | wc -l)" == 1 ]] || fail 'export antérieur affecté par le signal'
|
||||
check_hashes "$prior_dir"; no_repo_change; pass
|
||||
done
|
||||
|
||||
printf 'Résultat : %s scénarios synthétiques confinés validés.\n' "$passes"
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# All privileged calls are dispatched here inside the read-only bwrap sandbox.
|
||||
set -euo pipefail
|
||||
|
||||
name="${0##*/}"
|
||||
printf '%s' "$name" >> /mnt/calls
|
||||
printf ' %q' "$@" >> /mnt/calls
|
||||
printf '\n' >> /mnt/calls
|
||||
|
||||
if [[ "$name" == id ]]; then
|
||||
if [[ "${SIM_ID_ROOT:-0}" == 1 ]]; then
|
||||
case "${1:-}" in
|
||||
-un) printf 'root\n' ;;
|
||||
-u) printf '0\n' ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
else
|
||||
/usr/bin/id "$@"
|
||||
fi
|
||||
exit
|
||||
fi
|
||||
|
||||
mapped() {
|
||||
case "$1" in
|
||||
/root/arch-system-restore.*) printf '/mnt/backups%s' "${1#/root}" ;;
|
||||
/etc/*|/usr/local/sbin/*) printf '%s' "$1" ;;
|
||||
*) printf 'REFUSED destination: %s\n' "$1" >&2; exit 90 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "$name" == systemctl ]]; then
|
||||
action="${1:-}"
|
||||
case "$action" in
|
||||
cat)
|
||||
[[ "${SIM_MISSING_UNIT:-}" != "$2" ]] && grep -Fxq -- "$2" /mnt/units ;;
|
||||
is-enabled) printf '%s\n' "${SIM_UNIT_ENABLED:-disabled}" ;;
|
||||
is-active) printf '%s\n' "${SIM_UNIT_ACTIVE:-inactive}" ;;
|
||||
*) printf 'REFUSED systemctl read: %s\n' "$action" >&2; exit 90 ;;
|
||||
esac
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ "$name" == yay ]]; then
|
||||
[[ "${1:-}" == -S && "${2:-}" == --needed ]] || exit 90
|
||||
[[ "${SIM_FAIL_YAY:-0}" != 1 ]] || exit 41
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$name" != sudo ]]; then
|
||||
printf 'REFUSED direct mutation: %s\n' "$name" >&2
|
||||
exit 90
|
||||
fi
|
||||
|
||||
command_name="${1:-}"
|
||||
shift || true
|
||||
case "$command_name" in
|
||||
-v) exit 0 ;;
|
||||
/usr/bin/bash)
|
||||
# Reconnaître le fragment fixe sans jamais exécuter le code reçu.
|
||||
[[ $# == 4 && "$1" == -c && "$3" == -- ]] || exit 90
|
||||
digest="$(printf '%s' "$2" | /usr/bin/sha256sum)"
|
||||
[[ "${digest%% *}" == 88a1ea2034ba3685baa35be4404de4089207fd0c781f248d7cbc3f9c56e33b67 ]] || exit 90
|
||||
target="$4"
|
||||
[[ "$target" == /etc/* || "$target" == /usr/local/sbin/* ]] || exit 90
|
||||
count=0
|
||||
if [[ "${SIM_INSPECT_TARGET:-}" == "$target" ]]; then
|
||||
[[ ! -f /mnt/inspect-count ]] || read -r count < /mnt/inspect-count
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" > /mnt/inspect-count
|
||||
mode="${SIM_INSPECT_MODE:-}"
|
||||
if [[ "$mode" == second_fail && "$count" != 2 ]]; then mode=''; fi
|
||||
case "$mode" in
|
||||
fail) printf 'simulated sudo refusal\n' >&2; exit 1 ;;
|
||||
parent_fail) printf 'simulated parent inspection error\n' >&2; exit 3 ;;
|
||||
second_fail) printf 'simulated second inspection error\n' >&2; exit 3 ;;
|
||||
empty) exit 0 ;;
|
||||
unknown) printf 'UNKNOWN\n'; exit 0 ;;
|
||||
valid_fail) printf 'EXISTING\n'; exit 1 ;;
|
||||
'') ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
fi
|
||||
parent="${target%/*}"
|
||||
while [[ "$parent" != / ]]; do
|
||||
if [[ -L "$parent" ]]; then printf 'LINK\n'; exit 0; fi
|
||||
if [[ ! -e "$parent" ]]; then printf 'ABSENT\n'; exit 0; fi
|
||||
if [[ ! -d "$parent" ]]; then printf 'INVALID\n'; exit 0; fi
|
||||
parent="${parent%/*}"
|
||||
[[ -n "$parent" ]] || parent=/
|
||||
done
|
||||
if [[ -L "$target" ]]; then printf 'LINK\n'
|
||||
elif [[ -f "$target" ]]; then printf 'EXISTING\n'
|
||||
elif [[ -e "$target" ]]; then printf 'INVALID\n'
|
||||
else printf 'ABSENT\n'; fi ;;
|
||||
mktemp)
|
||||
[[ "$1" == -d && "$2" == '/root/arch-system-restore.XXXXXXXX' ]] || exit 90
|
||||
count=0
|
||||
[[ ! -f /mnt/counter ]] || read -r count < /mnt/counter
|
||||
count=$((count + 1))
|
||||
printf '%s\n' "$count" > /mnt/counter
|
||||
printf '/root/arch-system-restore.SIM%04d\n' "$count" ;;
|
||||
nft)
|
||||
[[ "$1" == -c && "$2" == -f && "$3" == /mnt/repo/rootfs/etc/nftables.conf ]] || exit 90
|
||||
[[ "${SIM_FAIL_NFT:-0}" != 1 ]] || exit 42 ;;
|
||||
visudo)
|
||||
[[ "$1" == -cf && "$2" == /tmp/* ]] || exit 90
|
||||
[[ "${SIM_FAIL_VISUDO:-0}" != 1 ]] || exit 43
|
||||
grep -Fq 'fixtureuser' "$2" ;;
|
||||
pacman)
|
||||
[[ "$1" == -Syu && "$2" == --needed ]] || exit 90
|
||||
[[ "${SIM_FAIL_PACMAN:-0}" != 1 ]] || exit 44 ;;
|
||||
systemctl)
|
||||
action="${1:-}"
|
||||
case "$action" in
|
||||
daemon-reload|enable|disable) ;;
|
||||
*) exit 90 ;;
|
||||
esac
|
||||
[[ "${SIM_FAIL_SYSTEMCTL:-}" != "$action" ]] || exit 45 ;;
|
||||
tlp)
|
||||
[[ "${1:-}" == start ]] || exit 90 ;;
|
||||
mkdir)
|
||||
[[ "$1" == -p && "$2" == /root/arch-system-restore.* ]] || exit 90
|
||||
/usr/bin/mkdir -p -- "$(mapped "$2")" ;;
|
||||
cp)
|
||||
[[ "$1" == -a && "$2" == -- ]] || exit 90
|
||||
src="$3" dest="$(mapped "$4")"
|
||||
[[ "$src" == /etc/* || "$src" == /usr/local/sbin/* ]] || exit 90
|
||||
/usr/bin/cp -a -- "$src" "$dest" ;;
|
||||
install)
|
||||
[[ "$1" == -D && "$2" == -m && $# == 5 ]] || exit 90
|
||||
mode="$3" src="$4" dest="$5"
|
||||
[[ "$src" == /mnt/repo/* || "$src" == /tmp/* || "$src" == /dev/null ]] || exit 90
|
||||
[[ "$dest" == /etc/* || "$dest" == /usr/local/sbin/* || "$dest" == /root/arch-system-restore.* ]] || exit 90
|
||||
[[ "${SIM_FAIL_INSTALL_TARGET:-}" != "$dest" ]] || exit 46
|
||||
if [[ "${SIM_INTERRUPT_TARGET:-}" == "$dest" ]]; then
|
||||
signal="${SIM_INTERRUPT_SIGNAL:-TERM}"
|
||||
[[ "$signal" == INT || "$signal" == TERM ]] || exit 90
|
||||
kill "-$signal" "$PPID"
|
||||
[[ "$signal" == INT ]] && exit 130 || exit 143
|
||||
fi
|
||||
/usr/bin/install -D -m "$mode" "$src" "$(mapped "$dest")" ;;
|
||||
*) printf 'REFUSED sudo subcommand: %s\n' "$command_name" >&2; exit 90 ;;
|
||||
esac
|
||||
|
|
@ -1,273 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Exécuter seulement une copie de deploy.sh dans un bwrap sans réseau, avec / en lecture seule.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
command -v bwrap >/dev/null || { echo 'bwrap requis pour le confinement' >&2; exit 1; }
|
||||
TEST_HOME="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$TEST_HOME"' EXIT
|
||||
passes=0
|
||||
case_dir=''
|
||||
rc=0
|
||||
|
||||
fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; }
|
||||
pass() { passes=$((passes + 1)); }
|
||||
expect_rc() { [[ "$rc" == "$1" ]] || fail "code $rc, attendu $1 ; sortie : $(cat "$case_dir/output")"; }
|
||||
contains() { grep -Fq -- "$1" "$case_dir/output" || fail "texte absent : $1"; }
|
||||
not_contains() { ! grep -Fq -- "$1" "$case_dir/output" || fail "texte interdit : $1"; }
|
||||
no_sudo() { ! grep -q '^sudo ' "$case_dir/calls" || fail 'appel sudo dans un scénario sans mutation'; }
|
||||
clean_nominal() {
|
||||
[[ ! -s "$case_dir/stderr" ]] || fail "stderr inattendu : $(cat "$case_dir/stderr")"
|
||||
! grep -Eq 'REFUSED|commande introuvable|command not found|ligne [0-9]+:' "$case_dir/output" ||
|
||||
fail 'diagnostic parasite dans un scénario nominal'
|
||||
}
|
||||
clean_error() {
|
||||
! grep -Eq 'REFUSED|commande introuvable|command not found|ligne [0-9]+:' "$case_dir/output" ||
|
||||
fail 'diagnostic parasite dans un scénario d’erreur'
|
||||
if grep -Ev '^\[!\] |^Option inconnue : |^simulated (sudo refusal|parent inspection error|second inspection error)$' "$case_dir/stderr" > "$case_dir/unexpected-stderr"; then
|
||||
fail "stderr non prévu : $(cat "$case_dir/unexpected-stderr")"
|
||||
fi
|
||||
}
|
||||
|
||||
new_case() {
|
||||
case_dir="$(mktemp -d "$TEST_HOME/case.XXXXXXXX")"
|
||||
mkdir -p "$case_dir"/{repo/scripts,repo/pkglist,repo/manifests,repo/templates/sudoers.d,repo/rootfs/etc/tlp.d,repo/rootfs/etc/systemd,repo/rootfs/etc/snapper/configs,repo/rootfs/usr/local/sbin,bin,etc/usr,usr-local-sbin,tmp,backups}
|
||||
cp "$ROOT/scripts/deploy.sh" "$case_dir/repo/scripts/deploy.sh"
|
||||
chmod 755 "$case_dir/repo/scripts/deploy.sh"
|
||||
for source in pacman.conf makepkg.conf mkinitcpio.conf nftables.conf limine-snapper-sync.conf; do
|
||||
printf 'fixture %s\n' "$source" > "$case_dir/repo/rootfs/etc/$source"
|
||||
done
|
||||
printf 'fixture tlp\n' > "$case_dir/repo/rootfs/etc/tlp.d/10-archasp.conf"
|
||||
printf 'fixture zram\n' > "$case_dir/repo/rootfs/etc/systemd/zram-generator.conf"
|
||||
printf 'fixture snapper\n' > "$case_dir/repo/rootfs/etc/snapper/configs/root"
|
||||
for helper in batlimit-set vmware-stack; do
|
||||
printf '#!/usr/bin/env bash\nexit 0\n' > "$case_dir/repo/rootfs/usr/local/sbin/$helper"
|
||||
done
|
||||
printf '@USER@ ALL=(root) NOPASSWD: /usr/local/sbin/batlimit-set\n' > "$case_dir/repo/templates/sudoers.d/batlimit"
|
||||
printf '@USER@ ALL=(root) NOPASSWD: /usr/local/sbin/vmware-stack start\n' > "$case_dir/repo/templates/sudoers.d/vmware-stack"
|
||||
printf '# comment\n alpha \n\n' > "$case_dir/repo/pkglist/pacman.txt"
|
||||
printf 'foreign\n' > "$case_dir/repo/pkglist/aur.txt"
|
||||
printf 'alpha.service\n' > "$case_dir/repo/manifests/system-services.txt"
|
||||
printf 'beta.timer\n' > "$case_dir/repo/manifests/system-timers.txt"
|
||||
printf 'ondemand.service\n' > "$case_dir/repo/manifests/on-demand-system-services.txt"
|
||||
printf 'alpha.service\nbeta.timer\nondemand.service\n' > "$case_dir/units"
|
||||
printf 'existing config\n' > "$case_dir/etc/pacman.conf"
|
||||
printf 'fixturehost\n' > "$case_dir/etc/hostname"
|
||||
uid="$(id -u)" gid="$(id -g)"
|
||||
printf 'fixtureuser:x:%s:%s:Fixture:/mnt:/usr/bin/bash\n' "$uid" "$gid" > "$case_dir/etc/passwd"
|
||||
printf 'fixture:x:%s:\n' "$gid" > "$case_dir/etc/group"
|
||||
cp /etc/nsswitch.conf "$case_dir/etc/nsswitch.conf"
|
||||
for safe in bash cat dirname find grep sed mktemp rm rmdir; do
|
||||
[[ -x "/usr/bin/$safe" ]] || fail "outil réel absent : /usr/bin/$safe"
|
||||
ln -s "/usr/bin/$safe" "$case_dir/bin/$safe"
|
||||
done
|
||||
for mocked in sudo systemctl yay pacman nft visudo tlp install id; do
|
||||
ln -s /mnt/mock-command.sh "$case_dir/bin/$mocked"
|
||||
done
|
||||
cp "$ROOT/tests/deploy/mock-command.sh" "$case_dir/mock-command.sh"
|
||||
chmod 755 "$case_dir/mock-command.sh"
|
||||
: > "$case_dir/calls"
|
||||
}
|
||||
|
||||
# Le bind de /etc et /usr/local/sbin fournit uniquement des cibles synthétiques.
|
||||
# / est en lecture seule, /tmp et /mnt sont les seuls espaces inscriptibles,
|
||||
# le réseau est absent, et PATH n'offre que des lectures sûres ou nos doublures.
|
||||
run_case() {
|
||||
local name="$1"
|
||||
shift
|
||||
: > "$case_dir/output"
|
||||
: > "$case_dir/stderr"
|
||||
set +e
|
||||
printf '%s\n' "${SIM_ANSWER:-y}" | bwrap --unshare-user --unshare-pid --unshare-net \
|
||||
--ro-bind / / --bind "$case_dir/etc" /etc \
|
||||
--bind "$case_dir/usr-local-sbin" /usr/local/sbin \
|
||||
--bind "$case_dir/tmp" /tmp --bind "$case_dir" /mnt \
|
||||
--proc /proc --dev /dev \
|
||||
--setenv PATH /mnt/bin --setenv HOME /mnt --setenv TMPDIR /tmp \
|
||||
--setenv SIM_FAIL_NFT "${SIM_FAIL_NFT:-0}" \
|
||||
--setenv SIM_FAIL_VISUDO "${SIM_FAIL_VISUDO:-0}" \
|
||||
--setenv SIM_FAIL_INSTALL_TARGET "${SIM_FAIL_INSTALL_TARGET:-}" \
|
||||
--setenv SIM_INTERRUPT_TARGET "${SIM_INTERRUPT_TARGET:-}" \
|
||||
--setenv SIM_INTERRUPT_SIGNAL "${SIM_INTERRUPT_SIGNAL:-TERM}" \
|
||||
--setenv SIM_INSPECT_TARGET "${SIM_INSPECT_TARGET:-}" \
|
||||
--setenv SIM_INSPECT_MODE "${SIM_INSPECT_MODE:-}" \
|
||||
--setenv SIM_FAIL_SYSTEMCTL "${SIM_FAIL_SYSTEMCTL:-}" \
|
||||
--setenv SIM_MISSING_UNIT "${SIM_MISSING_UNIT:-}" \
|
||||
--setenv SIM_UNIT_ENABLED "${SIM_UNIT_ENABLED:-disabled}" \
|
||||
--setenv SIM_UNIT_ACTIVE "${SIM_UNIT_ACTIVE:-inactive}" \
|
||||
--setenv SIM_ID_ROOT "${SIM_ID_ROOT:-0}" \
|
||||
--setenv USER root \
|
||||
-- /mnt/repo/scripts/deploy.sh "$@" > "$case_dir/stdout" 2> "$case_dir/stderr"
|
||||
rc=$?
|
||||
set -e
|
||||
cat "$case_dir/stdout" "$case_dir/stderr" > "$case_dir/output"
|
||||
printf '[test] %s : rc=%s\n' "$name" "$rc"
|
||||
}
|
||||
|
||||
new_case
|
||||
run_case help --help; expect_rc 0; no_sudo; clean_nominal; pass
|
||||
run_case no_selection; expect_rc 2; no_sudo; clean_error; pass
|
||||
run_case bad_option --unknown; expect_rc 2; no_sudo; clean_error; pass
|
||||
|
||||
for selection in --packages --configs --tools --services --all; do
|
||||
new_case
|
||||
run_case "dry_$selection" --dry-run "$selection"
|
||||
expect_rc 0; contains 'déploiement non effectué'; not_contains 'Déploiement sélectionné terminé'
|
||||
no_sudo
|
||||
clean_nominal
|
||||
if [[ "$selection" == --all ]]; then
|
||||
contains '/etc/snapper/configs/root'
|
||||
contains '/etc/sudoers.d/batlimit'
|
||||
contains 'Activer : alpha.service'
|
||||
fi
|
||||
[[ ! -e "$case_dir/counter" ]] || fail 'sauvegarde créée en simulation'
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire créé en simulation'
|
||||
pass
|
||||
done
|
||||
|
||||
new_case
|
||||
rm "$case_dir/repo/rootfs/etc/pacman.conf"
|
||||
run_case missing_source --dry-run --configs; expect_rc 1; contains 'source obligatoire'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
rm "$case_dir/repo/rootfs/etc/pacman.conf"
|
||||
run_case all_missing_source --all; expect_rc 1; contains 'source obligatoire'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
rm "$case_dir/repo/manifests/system-services.txt"
|
||||
run_case missing_manifest --dry-run --services; expect_rc 1; contains 'liste/manifest absent'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
printf 'bad option\n' >> "$case_dir/repo/pkglist/pacman.txt"
|
||||
run_case bad_line --dry-run --packages; expect_rc 1; contains 'nom de paquet invalide'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
: > "$case_dir/repo/pkglist/pacman.txt"
|
||||
: > "$case_dir/repo/pkglist/aur.txt"
|
||||
run_case empty_lists --dry-run --packages; expect_rc 0; contains '(0 entrées)'; no_sudo; clean_nominal; pass
|
||||
run_case empty_lists_apply --packages; expect_rc 0; clean_nominal
|
||||
! grep -q '^sudo pacman ' "$case_dir/calls" || fail 'pacman appelé pour liste vide'
|
||||
! grep -q '^yay ' "$case_dir/calls" || fail 'yay appelé pour liste vide'
|
||||
pass
|
||||
new_case
|
||||
rm "$case_dir/bin/yay"
|
||||
run_case no_yay --packages; expect_rc 1; contains 'commande requise absente : yay'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
SIM_ID_ROOT=1 run_case invalid_identity --tools; expect_rc 1; contains 'compte bénéficiaire root refusé'; no_sudo; clean_error; pass
|
||||
|
||||
new_case
|
||||
SIM_FAIL_NFT=1 run_case nft_rejected --configs; expect_rc 1; contains 'validation nftables'
|
||||
! grep -q '^sudo install ' "$case_dir/calls" || fail 'configuration installée après échec nft'; pass
|
||||
new_case
|
||||
SIM_FAIL_VISUDO=1 run_case sudoers_rejected --tools; expect_rc 1; contains 'validation sudoers'
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire sudoers non nettoyé'
|
||||
! grep -q '^sudo install ' "$case_dir/calls" || fail 'helper installé avant validation sudoers'; pass
|
||||
|
||||
new_case
|
||||
SIM_FAIL_INSTALL_TARGET=/etc/makepkg.conf run_case partial_after_packages --packages --configs
|
||||
expect_rc 1; contains 'Étapes achevées : paquets'; not_contains 'Déploiement sélectionné terminé'
|
||||
contains 'installation:/etc/pacman.conf'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'sauvegarde existante absente'
|
||||
clean_error; pass
|
||||
|
||||
# R1 : un échec du contrôle sudo/find, même avec un état plausible, n'est jamais ABSENT.
|
||||
for mode in fail parent_fail empty unknown valid_fail; do
|
||||
new_case
|
||||
SIM_INSPECT_TARGET=/etc/pacman.conf SIM_INSPECT_MODE="$mode" run_case "inspect_$mode" --configs
|
||||
expect_rc 1
|
||||
if [[ "$mode" == empty || "$mode" == unknown ]]; then
|
||||
contains 'réponse de contrôle invalide'
|
||||
else
|
||||
contains 'contrôle privilégié impossible'
|
||||
fi
|
||||
not_contains 'Déploiement sélectionné terminé'
|
||||
[[ "$(cat "$case_dir/etc/pacman.conf")" == 'existing config' ]] || fail 'cible existante modifiée après contrôle invalide'
|
||||
[[ ! -e "$case_dir/backups/arch-system-restore.SIM0001/absent/etc/pacman.conf" ]] || fail 'fausse absence après contrôle invalide'
|
||||
! grep -Fq 'sudo install -D -m 0644' "$case_dir/calls" || fail 'installation après contrôle invalide'
|
||||
clean_error; pass
|
||||
done
|
||||
new_case
|
||||
SIM_INSPECT_TARGET=/etc/pacman.conf SIM_INSPECT_MODE=second_fail run_case inspect_second_fail --configs
|
||||
expect_rc 1; contains 'contrôle privilégié impossible'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'sauvegarde perdue après second contrôle'
|
||||
[[ "$(cat "$case_dir/etc/pacman.conf")" == 'existing config' ]] || fail 'cible modifiée après second contrôle'
|
||||
! grep -Fq 'sudo install -D -m 0644 /mnt/repo/rootfs/etc/pacman.conf /etc/pacman.conf' "$case_dir/calls" || fail 'installation après second contrôle'
|
||||
clean_error; pass
|
||||
|
||||
new_case
|
||||
run_case first_backup --configs; expect_rc 0
|
||||
clean_nominal
|
||||
run_case second_backup --configs; expect_rc 0
|
||||
clean_nominal
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf" ]] || fail 'première sauvegarde absente'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/etc/makepkg.conf" ]] || fail 'marqueur de cible absente absent'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0002/existing/etc/makepkg.conf" ]] || fail 'seconde sauvegarde absente'
|
||||
[[ "$(cat "$case_dir/backups/arch-system-restore.SIM0001/existing/etc/pacman.conf")" == 'existing config' ]] || fail 'sauvegarde initiale incorrecte'
|
||||
[[ "$(cat "$case_dir/etc/makepkg.conf")" == 'fixture makepkg.conf' ]] || fail 'installation de cible absente manquante'
|
||||
pass
|
||||
new_case
|
||||
rm "$case_dir/etc/pacman.conf"
|
||||
ln -s /mnt/escape "$case_dir/etc/pacman.conf"
|
||||
run_case dangling_target --dry-run --configs; expect_rc 1; contains 'cible liée refusée'; no_sudo; pass
|
||||
new_case
|
||||
ln -s /mnt/escape "$case_dir/etc/tlp.d"
|
||||
run_case linked_parent --dry-run --configs; expect_rc 1; contains 'répertoire cible lié refusé'; no_sudo; pass
|
||||
|
||||
new_case
|
||||
SIM_FAIL_INSTALL_TARGET=/usr/local/sbin/batlimit-set run_case temp_cleanup_failure --tools
|
||||
expect_rc 1
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé après erreur'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/usr/local/sbin/batlimit-set" ]] || fail 'marqueur supprimé après erreur'
|
||||
pass
|
||||
new_case
|
||||
SIM_INTERRUPT_TARGET=/usr/local/sbin/batlimit-set run_case term_cleanup --tools
|
||||
expect_rc 143
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé après TERM'
|
||||
contains 'Interruption TERM (143)'; contains 'Sauvegardes et marqueurs conservés'; clean_error; pass
|
||||
for signal in INT TERM; do
|
||||
new_case
|
||||
SIM_INTERRUPT_TARGET=/usr/local/sbin/vmware-stack SIM_INTERRUPT_SIGNAL="$signal" run_case "interrupt_second_$signal" --tools
|
||||
if [[ "$signal" == INT ]]; then expect_rc 130; else expect_rc 143; fi
|
||||
contains "Interruption $signal"; contains 'phase : outils root'
|
||||
contains 'installation:/usr/local/sbin/batlimit-set'
|
||||
contains 'Action interrompue, effets à vérifier : installation /usr/local/sbin/vmware-stack'
|
||||
contains 'Sauvegardes et marqueurs conservés : /root/arch-system-restore.SIM0001'
|
||||
not_contains 'Déploiement sélectionné terminé'
|
||||
[[ -e "$case_dir/usr-local-sbin/batlimit-set" ]] || fail 'premier helper non installé avant interruption'
|
||||
[[ -e "$case_dir/backups/arch-system-restore.SIM0001/absent/usr/local/sbin/vmware-stack" ]] || fail 'marqueur non conservé'
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaires non nettoyés après signal'
|
||||
clean_error; pass
|
||||
done
|
||||
|
||||
new_case
|
||||
rm "$case_dir/etc/hostname"
|
||||
run_case missing_hostname --configs; expect_rc 1; contains '/etc/hostname illisible ou absent'; no_sudo; clean_error; pass
|
||||
new_case
|
||||
SIM_ANSWER=n run_case user_cancel --configs; expect_rc 0; contains 'Déploiement annulé'; no_sudo; clean_nominal; pass
|
||||
|
||||
new_case
|
||||
SIM_MISSING_UNIT=alpha.service run_case missing_unit --dry-run --services
|
||||
expect_rc 1; contains 'unité requise absente'; no_sudo; pass
|
||||
new_case
|
||||
SIM_FAIL_SYSTEMCTL=enable run_case failed_enable --services
|
||||
expect_rc 1; contains 'activation alpha.service'; not_contains 'Déploiement sélectionné terminé'; pass
|
||||
new_case
|
||||
SIM_UNIT_ENABLED=enabled SIM_FAIL_SYSTEMCTL=disable run_case failed_disable --tools
|
||||
expect_rc 1; contains 'désactivation ondemand.service'; not_contains 'Déploiement sélectionné terminé'; pass
|
||||
new_case
|
||||
SIM_MISSING_UNIT=alpha.service run_case deferred_unit_failure --packages --services
|
||||
expect_rc 1; contains 'Étapes achevées : paquets'; contains 'unité requise absente'; pass
|
||||
new_case
|
||||
run_case tools_identity --tools; expect_rc 0
|
||||
clean_nominal
|
||||
grep -Fq 'fixtureuser ALL=' "$case_dir/etc/sudoers.d/batlimit" || fail 'sudoers utilisateur erroné'
|
||||
[[ -z "$(ls -A "$case_dir/tmp")" ]] || fail 'temporaire non nettoyé en réussite'
|
||||
pass
|
||||
new_case
|
||||
run_case all_order --all; expect_rc 0
|
||||
clean_nominal
|
||||
python3 - "$case_dir/calls" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
s=Path(sys.argv[1]).read_text()
|
||||
for a,b in [('sudo pacman -Syu','yay -S'),('yay -S','sudo install -D'),('sudo install -D','sudo systemctl enable')]:
|
||||
if s.find(a)<0 or s.find(b)<0 or s.find(a)>=s.find(b): raise SystemExit(f'order failure: {a} -> {b}')
|
||||
PY
|
||||
pass
|
||||
printf 'Résultat : %s scénarios validés dans bwrap ; aucun script du dépôt exécuté hors copie.\n' "$passes"
|
||||
|
|
@ -1,136 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Strict synthetic command boundary; only the private /tmp fixture is writable."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import sys
|
||||
|
||||
name = Path(sys.argv[0]).name
|
||||
args = sys.argv[1:]
|
||||
root = Path('/tmp/fixture')
|
||||
state_path = root / 'state.json'
|
||||
state = json.loads(state_path.read_text())
|
||||
fail = os.environ.get('CASE_FAIL', '')
|
||||
nvme = os.environ.get('CASE_STYLE', '') == 'nvme'
|
||||
disk_path = '/dev/nvme9n9' if nvme else '/dev/target'
|
||||
efi_path = disk_path + ('p1' if nvme else '1')
|
||||
root_path = disk_path + ('p2' if nvme else '2')
|
||||
with (root / 'calls').open('a') as stream:
|
||||
stream.write(name + ' ' + ' '.join(args) + '\n')
|
||||
|
||||
def save():
|
||||
state_path.write_text(json.dumps(state))
|
||||
|
||||
def die(message):
|
||||
print(message, file=sys.stderr)
|
||||
sys.exit(47)
|
||||
|
||||
if fail == name or fail == name + ':inspection' and name in ('lsblk', 'findmnt', 'swapon'):
|
||||
die('injected failure')
|
||||
if name == 'readlink':
|
||||
if args[:2] != ['-e','--'] or len(args) != 3 or args[2] not in (disk_path,'/dev/alias',efi_path):
|
||||
die('unexpected readlink')
|
||||
print(efi_path if args[2] == efi_path else disk_path)
|
||||
elif name == 'lsblk':
|
||||
if args[:5] != ['--json','--tree','--bytes','--output','PATH,TYPE,MAJ:MIN,SIZE,MODEL,SERIAL,WWN,MOUNTPOINTS,PKNAME,PARTN'] or len(args) != 6:
|
||||
die('unexpected lsblk args')
|
||||
target = args[5]
|
||||
state['inspections'] = state.get('inspections', 0) + (target == disk_path)
|
||||
save()
|
||||
disk = {'path':disk_path,'type':'disk','maj:min':'8:16','size':100*1024**3,'model':'SYNTHETIC','serial':'TEST-NOT-REAL','wwn':None,'mountpoints':[None]}
|
||||
if fail == 'identity_change' and state['inspections'] >= 2:
|
||||
disk['maj:min'] = '8:20'
|
||||
if state['partitioned']:
|
||||
disk['children'] = [
|
||||
{'path':efi_path,'type':'part','maj:min':'8:17','size':1024**3,'partn':1,'pkname':Path(disk_path).name,'mountpoints':[None]},
|
||||
{'path':root_path,'type':'part','maj:min':'8:18','size':99*1024**3,'partn':2,'pkname':Path(disk_path).name,'mountpoints':[None]},
|
||||
]
|
||||
if fail == 'bad_partitions': disk['children'][1]['partn'] = 3
|
||||
if fail == 'mounted_child': disk['children'] = [{'path':'/dev/target1','type':'part','maj:min':'8:17','size':1024**3,'mountpoints':['/work']}]
|
||||
if fail == 'wrong_type': disk['type'] = 'rom'
|
||||
if target == efi_path: disk = {'path':efi_path,'type':'part','maj:min':'8:17','size':1024**3,'mountpoints':[None]}
|
||||
if target == '/dev/live': disk = {'path':'/dev/live','type':'part','maj:min':'8:99','size':8*1024**3,'pkname':'live','mountpoints':['/run/archiso/bootmnt']}
|
||||
if target not in (disk_path,efi_path,'/dev/live'): die('unexpected lsblk target')
|
||||
print(json.dumps({'blockdevices':[disk]}))
|
||||
elif name == 'findmnt':
|
||||
if args != ['--json','--output','TARGET,SOURCE,MAJ:MIN,FSTYPE,OPTIONS,ID']:
|
||||
die('unexpected findmnt')
|
||||
entries = [{'target':'/','source':'/dev/live','maj:min':'8:99','fstype':'ext4','options':'rw'}, {'target':'/run/archiso','source':'/dev/live','maj:min':'8:99','fstype':'ext4','options':'rw'}]
|
||||
if fail in ('overlay_live','overlay_unmapped'):
|
||||
entries[0].update({'source':'overlay','maj:min':'0:99','fstype':'overlay',
|
||||
'options':'rw,lowerdir=/run/archiso/bootmnt/airootfs,upperdir=/run/archiso/cowspace/upper,workdir=/run/archiso/cowspace/work'})
|
||||
if fail == 'overlay_live':
|
||||
entries.append({'target':'/run/archiso/bootmnt','source':'/dev/live','maj:min':'8:99','fstype':'iso9660','options':'ro'})
|
||||
entries.append({'target':'/run/archiso/cowspace','source':'tmpfs','maj:min':'0:98','fstype':'tmpfs','options':'rw'})
|
||||
if fail == 'source_target': entries[0]['maj:min'] = '8:16'
|
||||
if fail == 'mounted_disk': entries.append({'target':'/work','source':'/dev/target1','maj:min':'8:17','fstype':'ext4'})
|
||||
if fail == 'mnt_occupied': entries.append({'target':'/mnt/other','source':'/dev/live','maj:min':'8:99','fstype':'ext4'})
|
||||
if fail == 'swapfile': entries.append({'target':'/work','source':'/dev/target2','maj:min':'8:18','fstype':'ext4'})
|
||||
entries += state['mounts']
|
||||
print(json.dumps({'filesystems':entries}))
|
||||
elif name == 'swapon':
|
||||
if args != ['--show=NAME','--noheadings','--raw']: die('unexpected swapon')
|
||||
if fail == 'swap_partition': print(efi_path)
|
||||
elif fail == 'swapfile': print('/work/swapfile')
|
||||
elif name in ('wipefs','parted','partprobe','udevadm','mkfs.fat','mkfs.btrfs','btrfs','mount','umount'):
|
||||
if fail == name or fail == name + ':mutate': die('injected mutation failure')
|
||||
if name == 'wipefs' and args != ['-af','--',disk_path]: die('unexpected wipefs')
|
||||
if name == 'parted':
|
||||
allowed = (
|
||||
['-s',disk_path,'mklabel','gpt'],
|
||||
['-s',disk_path,'mkpart','ESP','fat32','1MiB','1025MiB'],
|
||||
['-s',disk_path,'set','1','esp','on'],
|
||||
['-s',disk_path,'mkpart','ARCH','btrfs','1025MiB','100%'],
|
||||
)
|
||||
if args not in allowed: die('unexpected parted')
|
||||
if args == ['-s',disk_path,'mkpart','ARCH','btrfs','1025MiB','100%']:
|
||||
state['partitioned'] = True; save()
|
||||
if name == 'partprobe' and args != [disk_path]: die('unexpected partprobe')
|
||||
if name == 'partprobe' and fail in ('signal_INT','signal_TERM'):
|
||||
os.kill(os.getppid(), signal.SIGINT if fail == 'signal_INT' else signal.SIGTERM)
|
||||
sys.exit(130 if fail == 'signal_INT' else 143)
|
||||
if name == 'udevadm' and args != ['settle']: die('unexpected udevadm')
|
||||
if name == 'mkfs.fat' and args != ['-F','32','-n','EFI',efi_path]: die('unexpected mkfs.fat')
|
||||
if name == 'mkfs.btrfs' and args != ['-f','-L','ARCH',root_path]: die('unexpected mkfs.btrfs')
|
||||
if name == 'btrfs':
|
||||
if fail == 'snapshot_conflict' and args[-1] == '/mnt/@/.snapshots': Path(args[-1]).mkdir(exist_ok=True)
|
||||
allowed = {'/mnt/@','/mnt/@home','/mnt/@log','/mnt/@pkg','/mnt/@/.snapshots'}
|
||||
if args[:2] != ['subvolume','create'] or len(args) != 3 or args[2] not in allowed or Path(args[2]).exists():
|
||||
die('subvolume destination already exists or invalid')
|
||||
Path(args[2]).mkdir()
|
||||
if name == 'mount':
|
||||
if fail in ('mount:third','mount_cleanup_fail','mount_replaced') and len(state['mounts']) == 1 and state.get('mount_count', 0) >= 2:
|
||||
if fail == 'mount_replaced':
|
||||
state['mounts'][0]['id'] += 500
|
||||
save()
|
||||
die('injected mount failure')
|
||||
if args[0] == '-o':
|
||||
expected_options = {'rw,relatime,ssd,discard=async,space_cache=v2,subvol='+v for v in ('@','@home','@log','@pkg','@/.snapshots')}
|
||||
if len(args) != 4 or args[1] not in expected_options:
|
||||
die('unexpected mount options')
|
||||
source, target = args[2:]
|
||||
options = args[1]
|
||||
else:
|
||||
if len(args) != 2: die('unexpected mount')
|
||||
source, target = args
|
||||
options = 'rw'
|
||||
allowed_mounts = {('/mnt',root_path),('/mnt/home',root_path),('/mnt/var/log',root_path),
|
||||
('/mnt/var/cache/pacman/pkg',root_path),('/mnt/.snapshots',root_path),
|
||||
('/mnt/boot',efi_path)}
|
||||
if (target,source) not in allowed_mounts:
|
||||
die('unexpected mount source/target')
|
||||
if fail == 'canonical_subvol':
|
||||
options = options.replace('subvol=', 'subvol=/')
|
||||
state['mount_count'] = state.get('mount_count', 0) + 1
|
||||
state['mounts'].append({'target':target,'source':source,'maj:min':'8:17' if source.endswith('1') else '8:18','fstype':'btrfs' if source.endswith('2') else 'vfat','options':options,'id':100+state['mount_count']})
|
||||
save()
|
||||
if name == 'umount':
|
||||
allowed_targets = {'/mnt','/mnt/home','/mnt/var/log','/mnt/var/cache/pacman/pkg','/mnt/.snapshots','/mnt/boot'}
|
||||
if len(args) != 2 or args[0] != '--' or args[1] not in allowed_targets: die('unexpected umount')
|
||||
if fail == 'mount_cleanup_fail' and args[1] == '/mnt' and state.get('mount_count', 0) >= 2:
|
||||
die('injected cleanup failure')
|
||||
state['mounts'] = [m for m in state['mounts'] if m['target'] != args[1]]
|
||||
save()
|
||||
else:
|
||||
die('unknown command')
|
||||
|
|
@ -1,132 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Copies and strict mocks only: no host block device or privileged command is touched.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
command -v bwrap >/dev/null || { echo 'bwrap requis' >&2; exit 1; }
|
||||
BASE="$(mktemp -d)"
|
||||
trap 'rm -rf -- "$BASE"' EXIT
|
||||
passes=0
|
||||
fail() { printf 'ÉCHEC : %s\n' "$*" >&2; exit 1; }
|
||||
new_case() {
|
||||
case_dir="$(mktemp -d "$BASE/case.XXXXXXXX")"
|
||||
mkdir -p "$case_dir"/{repo/scripts,bin,mnt,tmp/fixture,sys/dev/block,sys/firmware/efi,run/archiso,run/lock}
|
||||
mkdir -p "$case_dir/run/archiso"/{bootmnt/airootfs,cowspace/upper,cowspace/work}
|
||||
for mm in 8:16 8:17 8:18 8:20; do mkdir -p "$case_dir/sys/dev/block/$mm/holders"; done
|
||||
cp "$ROOT/scripts/prepare-disk.sh" "$ROOT/scripts/prepare-disk.py" "$case_dir/repo/scripts/"
|
||||
cp "$ROOT/tests/prepare-disk/mock-command.py" "$case_dir/mock-command.py"
|
||||
for cmd in readlink lsblk findmnt swapon wipefs parted partprobe udevadm mkfs.fat mkfs.btrfs btrfs mount umount; do
|
||||
ln -s /opt/mock-command.py "$case_dir/bin/$cmd"
|
||||
done
|
||||
for cmd in bash python3 dirname mkdir; do ln -s "/usr/bin/$cmd" "$case_dir/bin/$cmd"; done
|
||||
printf '{"partitioned":false,"mounts":[]}' > "$case_dir/tmp/fixture/state.json"
|
||||
: > "$case_dir/tmp/fixture/calls"
|
||||
}
|
||||
run_case() {
|
||||
local label="$1"; shift
|
||||
local mnt_args=(--bind "$case_dir/mnt" /mnt)
|
||||
[[ "${CASE_MNT_LINK:-0}" != 1 ]] || mnt_args=(--symlink /tmp/fixture /mnt)
|
||||
set +e
|
||||
printf '%s\n' "${ANSWER:-}" | bwrap --unshare-user --unshare-pid --unshare-net --uid "${TEST_UID:-0}" --gid "${TEST_UID:-0}" \
|
||||
--tmpfs / --ro-bind /usr /usr --symlink usr/lib /lib --symlink usr/lib /lib64 \
|
||||
--symlink usr/bin /bin "${mnt_args[@]}" --bind "$case_dir/tmp" /tmp \
|
||||
--bind "$case_dir/sys" /sys --bind "$case_dir/run" /run --tmpfs /opt \
|
||||
--ro-bind "$case_dir/repo" /opt/repo --ro-bind "$case_dir/mock-command.py" /opt/mock-command.py \
|
||||
--ro-bind "$case_dir/bin" /opt/bin --dev /dev --proc /proc \
|
||||
--setenv PATH /opt/bin --setenv CASE_FAIL "${CASE_FAIL:-}" --setenv CASE_STYLE "${CASE_STYLE:-}" \
|
||||
-- /opt/repo/scripts/prepare-disk.sh "$@" > "$case_dir/out" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
printf '[test] %s : rc=%s\n' "$label" "$rc"
|
||||
}
|
||||
no_mutation() { ! grep -Eq '^(wipefs|parted|partprobe|udevadm|mkfs\.fat|mkfs\.btrfs|btrfs|mount|umount) ' "$case_dir/tmp/fixture/calls" || fail 'mutation imprévue'; }
|
||||
expect() { [[ "$rc" == "$1" ]] || fail "code $rc attendu $1: $(cat "$case_dir/out")"; }
|
||||
new_case; run_case help --help; expect 0; no_mutation; ((passes+=1))
|
||||
new_case; TEST_UID=1000 run_case dry --dry-run /dev/target; expect 0; no_mutation; ((passes+=1))
|
||||
new_case; CASE_FAIL=overlay_live TEST_UID=1000 run_case dry_overlay --dry-run /dev/target
|
||||
expect 0; no_mutation; ((passes+=1))
|
||||
for scenario in wrong_type mounted_child mounted_disk source_target swap_partition swapfile mnt_occupied overlay_unmapped lsblk:inspection findmnt:inspection swapon:inspection; do
|
||||
new_case; CASE_FAIL="$scenario" run_case "$scenario" /dev/target; expect 1; no_mutation; ((passes+=1))
|
||||
done
|
||||
new_case; touch "$case_dir/sys/dev/block/8:16/holders/dm-test"
|
||||
run_case holder /dev/target; expect 1; grep -q 'holder actif' "$case_dir/out" || fail 'holder non diagnostiqué'; no_mutation; ((passes+=1))
|
||||
new_case; rmdir "$case_dir/sys/dev/block/8:16/holders"
|
||||
run_case holders_unreadable /dev/target; expect 1; grep -q 'holders illisibles' "$case_dir/out" || fail 'lecture holders ignorée'; no_mutation; ((passes+=1))
|
||||
new_case; touch "$case_dir/mnt/old-data"
|
||||
run_case mnt_data /dev/target; expect 1; grep -q '/mnt lié, absent ou non vide' "$case_dir/out" || fail 'données /mnt ignorées'; no_mutation; ((passes+=1))
|
||||
new_case; CASE_MNT_LINK=1 run_case mnt_link /dev/target; expect 1
|
||||
grep -q '/mnt lié, absent ou non vide' "$case_dir/out" || fail 'lien /mnt ignoré'; no_mutation; ((passes+=1))
|
||||
new_case; rm "$case_dir/bin/parted"; ANSWER='ERASE /dev/target 8:16' run_case missing_dependency /dev/target
|
||||
expect 1; grep -q 'dépendance absente : parted' "$case_dir/out" || fail 'dépendance non diagnostiquée'; no_mutation; ((passes+=1))
|
||||
new_case; rm -r "$case_dir/run/archiso"; run_case no_live /dev/target
|
||||
expect 1; grep -q 'live Arch UEFI' "$case_dir/out" || fail 'contexte live non vérifié'; no_mutation; ((passes+=1))
|
||||
new_case; run_case partition /dev/target1; expect 1; no_mutation; ((passes+=1))
|
||||
new_case; CASE_FAIL=source_target run_case alias_source /dev/alias; expect 1
|
||||
grep -q 'disque ou descendant monté' "$case_dir/out" || fail 'alias source non protégé'; no_mutation; ((passes+=1))
|
||||
new_case; CASE_FAIL=readlink run_case alias_missing /dev/alias; expect 1; no_mutation; ((passes+=1))
|
||||
new_case; ANSWER=no run_case refusal /dev/target; expect 1; no_mutation; ((passes+=1))
|
||||
new_case; run_case eof /dev/target; expect 1; no_mutation; ((passes+=1))
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=identity_change run_case identity_change /dev/target
|
||||
expect 1; grep -q 'identité changée' "$case_dir/out" || fail 'changement identité non bloqué'; no_mutation; ((passes+=1))
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' run_case nominal /dev/alias; expect 0
|
||||
[[ "$(grep -c '^mount ' "$case_dir/tmp/fixture/calls")" == 7 ]] || fail 'montages incomplets'
|
||||
[[ -d "$case_dir/mnt/@/.snapshots" ]] || fail 'sous-volume imbriqué absent'
|
||||
((passes+=1))
|
||||
new_case; CASE_STYLE=nvme ANSWER='ERASE /dev/nvme9n9 8:16' run_case nominal_nvme /dev/nvme9n9; expect 0
|
||||
grep -q '^mkfs.fat -F 32 -n EFI /dev/nvme9n9p1$' "$case_dir/tmp/fixture/calls" || fail 'partition NVMe non vérifiée'
|
||||
((passes+=1))
|
||||
new_case; CASE_FAIL=overlay_live ANSWER='ERASE /dev/target 8:16' run_case nominal_overlay /dev/target
|
||||
expect 0; ((passes+=1))
|
||||
new_case; CASE_FAIL=canonical_subvol ANSWER='ERASE /dev/target 8:16' run_case canonical_subvol /dev/target
|
||||
expect 0; ((passes+=1))
|
||||
new_case
|
||||
exec 9> "$case_dir/run/lock/arch-system-prepare-8_16.lock"
|
||||
flock -n 9 || fail 'verrou fixture indisponible'
|
||||
ANSWER='ERASE /dev/target 8:16' run_case concurrent /dev/target; expect 1
|
||||
grep -q 'autre invocation coopérante' "$case_dir/out" || fail 'concurrence non bloquée'
|
||||
no_mutation
|
||||
exec 9>&-
|
||||
((passes+=1))
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=bad_partitions run_case bad_partitions /dev/target; expect 1
|
||||
! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage malgré partitions invalides'
|
||||
((passes+=1))
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=mkfs.btrfs run_case fail_mkfs /dev/target; expect 1
|
||||
! grep -q '^mount ' "$case_dir/tmp/fixture/calls" || fail 'montage après formatage échoué'
|
||||
((passes+=1))
|
||||
for scenario in wipefs parted partprobe udevadm mkfs.fat btrfs; do
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "fail_$scenario" /dev/target; expect 1
|
||||
grep -q "^$scenario " "$case_dir/tmp/fixture/calls" || fail "étape $scenario non atteinte"
|
||||
grep -q "étape à vérifier" "$case_dir/out" || fail 'bilan d’échec absent'
|
||||
case "$scenario" in
|
||||
wipefs) ! grep -q '^parted ' "$case_dir/tmp/fixture/calls" || fail 'parted après wipefs échoué' ;;
|
||||
parted) [[ "$(grep -c '^parted ' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'parted poursuivi après échec' ;;
|
||||
partprobe) ! grep -q '^udevadm ' "$case_dir/tmp/fixture/calls" || fail 'udev après partprobe échoué' ;;
|
||||
udevadm) ! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage après udev échoué' ;;
|
||||
mkfs.fat) ! grep -q '^mkfs.btrfs ' "$case_dir/tmp/fixture/calls" || fail 'Btrfs après FAT32 échoué' ;;
|
||||
btrfs) [[ "$(grep -c '^btrfs ' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'sous-volume poursuivi après échec'
|
||||
grep -q '^umount -- /mnt$' "$case_dir/tmp/fixture/calls" || fail 'montage top-level non nettoyé' ;;
|
||||
esac
|
||||
((passes+=1))
|
||||
done
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL=snapshot_conflict run_case snapshot_conflict /dev/target; expect 1
|
||||
grep -q 'subvolume destination already exists' "$case_dir/out" || fail 'précondition .snapshots non testée'
|
||||
! grep -q '^mount -o' "$case_dir/tmp/fixture/calls" || fail 'montage final malgré conflit .snapshots'
|
||||
((passes+=1))
|
||||
for scenario in mount:third mount_cleanup_fail mount_replaced; do
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "$scenario" /dev/target; expect 1
|
||||
if [[ "$scenario" == mount:third ]]; then
|
||||
grep -q '^umount -- /mnt$' "$case_dir/tmp/fixture/calls" || fail 'nettoyage ciblé absent'
|
||||
elif [[ "$scenario" == mount_cleanup_fail ]]; then
|
||||
grep -q 'Montages restants à examiner : /mnt' "$case_dir/out" || fail 'échec nettoyage non signalé'
|
||||
else
|
||||
grep -q 'montage à examiner, non démonté : /mnt' "$case_dir/out" || fail 'montage remplacé démonté'
|
||||
[[ "$(grep -c '^umount -- /mnt$' "$case_dir/tmp/fixture/calls")" == 1 ]] || fail 'montage remplacé démonté'
|
||||
fi
|
||||
((passes+=1))
|
||||
done
|
||||
for scenario in signal_INT signal_TERM; do
|
||||
new_case; ANSWER='ERASE /dev/target 8:16' CASE_FAIL="$scenario" run_case "$scenario" /dev/target
|
||||
[[ "$rc" == 130 || "$rc" == 143 ]] || fail "code interruption inattendu : $rc"
|
||||
! grep -q '^mkfs' "$case_dir/tmp/fixture/calls" || fail 'formatage après interruption'
|
||||
((passes+=1))
|
||||
done
|
||||
printf 'Résultat : %s scénarios confinés de préparation validés.\n' "$passes"
|
||||
16
tests/run.sh
16
tests/run.sh
|
|
@ -1,16 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Point d'entrée unique : les suites de production s'exécutent sur copies bwrap.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
"$ROOT/scripts/check-repo.sh"
|
||||
for suite in backup deploy prepare-disk audit; do
|
||||
printf '[suite] %s\n' "$suite"
|
||||
bash "$ROOT/tests/$suite/test.sh"
|
||||
done
|
||||
if command -v shellcheck >/dev/null 2>&1; then
|
||||
mapfile -d '' scripts < <(find "$ROOT/scripts" "$ROOT/tests" -type f -name '*.sh' -print0)
|
||||
shellcheck "${scripts[@]}"
|
||||
else
|
||||
printf '[info] ShellCheck absent : contrôle non exécuté.\n'
|
||||
fi
|
||||
printf '[+] Toutes les suites disponibles ont réussi ; aucun déploiement ni réinstallation réelle.\n'
|
||||
Loading…
Reference in a new issue