feat(reconstruction): finaliser la préparation, les contrôles et les procédures
This commit is contained in:
parent
473b98a17b
commit
6b4bea0ae9
17 changed files with 1243 additions and 370 deletions
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -12,3 +12,15 @@
|
||||||
- remplacement de la configuration TLP monolithique par un drop-in minimal ;
|
- remplacement de la configuration TLP monolithique par un drop-in minimal ;
|
||||||
- retrait du swapfile de 16 Gio, maintien de zram 6 Gio ;
|
- 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 %.
|
- 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.
|
||||||
|
|
|
||||||
122
README.md
122
README.md
|
|
@ -1,92 +1,56 @@
|
||||||
# Arch System
|
# Arch System
|
||||||
|
|
||||||
Configuration et outillage de reconstruction de mon poste **ArchASP**.
|
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.
|
||||||
|
|
||||||
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é.
|
## Parcours
|
||||||
|
|
||||||
## Objectifs
|
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`.
|
||||||
|
|
||||||
- documenter l’état réel du poste ;
|
## Contrôles locaux
|
||||||
- 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.
|
|
||||||
|
|
||||||
La [référence de reconstruction](docs/reconstruction-baseline.md) distingue la cible
|
|
||||||
documentée de l’état observé sur ArchASP le 2 octobre 2026. Son
|
|
||||||
[suivi d’audit](docs/audits/2026-10-02-followup.md) précise les preuves, les
|
|
||||||
limites d’accès et les décisions encore ouvertes. Les inventaires du 26 juillet
|
|
||||||
restent des observations datées, pas une cible automatique de déploiement.
|
|
||||||
|
|
||||||
## Mise à jour du dépôt
|
|
||||||
|
|
||||||
**Changement de comportement :** sans argument, `backup.sh` crée désormais un
|
|
||||||
candidat d’export **privé hors du dépôt** pour revue. Il ne modifie plus
|
|
||||||
automatiquement les listes, configurations ou inventaires suivis par Git et
|
|
||||||
ne sauvegarde ni données personnelles ni clés. Sa présence locale ne prouve
|
|
||||||
pas une copie hors disque. Voir le [contrat d’export](docs/backup.md).
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/check-repo.sh
|
bash tests/run.sh
|
||||||
./scripts/backup.sh --dry-run
|
|
||||||
# Après décision explicite de collecter sur la machine concernée :
|
|
||||||
./scripts/backup.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Relire le manifeste et **tous** les fichiers candidats avant toute reprise
|
Ce point d’entrée vérifie la syntaxe des scripts, les listes, manifests,
|
||||||
manuelle dans Git ou publication : noms de services, métadonnées de stockage,
|
placeholders et liens locaux, puis les suites confinées de déploiement,
|
||||||
configurations et modèle Limine peuvent contenir des informations sensibles ou
|
d’export, de préparation de disque et de diagnostic. Les tests exécutent des
|
||||||
propres à la machine. Aucun candidat n’est promu automatiquement en référence
|
**copies** dans `bwrap`, sans réseau ni accès aux périphériques blocs hôtes.
|
||||||
de reconstruction.
|
ShellCheck est exécuté seulement s’il est déjà disponible. Une réussite ne
|
||||||
|
certifie ni démarrage ni restauration physique.
|
||||||
|
|
||||||
## Restauration
|
`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.
|
||||||
|
|
||||||
Le déploiement est volontairement découpé :
|
## Export privé et portée des fichiers
|
||||||
|
|
||||||
```bash
|
`scripts/backup.sh` crée un candidat **privé hors Git** pour revue. Il ne met
|
||||||
./scripts/deploy.sh --packages
|
pas à jour automatiquement `pkglist/`, `inventory/`, `rootfs/` ou `templates/`
|
||||||
./scripts/deploy.sh --configs
|
et n’est ni une sauvegarde des données ni une copie hors SSD. Lire son
|
||||||
./scripts/deploy.sh --tools
|
[contrat](docs/backup.md) et inspecter tout candidat avant promotion ou
|
||||||
./scripts/deploy.sh --services
|
publication ; les fichiers peuvent contenir des informations sensibles.
|
||||||
```
|
|
||||||
|
|
||||||
Pour tout appliquer :
|
`rootfs/` regroupe des configurations déployables **à sélectionner**, non
|
||||||
|
une vérité courante. `manifests/` décrit des intentions datées ; `inventory/`
|
||||||
```bash
|
conserve des observations. `templates/` contient des placeholders à remplacer
|
||||||
./scripts/deploy.sh --all
|
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)
|
||||||
`/etc/fstab`, `limine.conf`, les clés Secure Boot et les connexions réseau ne sont jamais restaurés automatiquement.
|
précisent les limites.
|
||||||
|
|
||||||
## Socle historiquement documenté
|
|
||||||
|
|
||||||
- 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 ; l’affirmation ancienne « sans swapfile permanent » ne décrit
|
|
||||||
plus à elle seule le poste observé le 2 octobre 2026 ;
|
|
||||||
- TLP avec seuil de charge ASUS à 60 % ;
|
|
||||||
- Nextcloud, Proton Mail Bridge, VMware et Samba lancés uniquement à la demande.
|
|
||||||
|
|
||||||
Ce socle n’est pas une certification de réinstallation. Le poste observé possède
|
|
||||||
aussi des swaps supplémentaires, CUPS disponible au démarrage et nginx utilisé
|
|
||||||
localement pour Trainlog. Leur portée sur une future installation reste à
|
|
||||||
décider dans la référence de reconstruction.
|
|
||||||
|
|
||||||
## 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é.
|
|
||||||
|
|
|
||||||
38
docs/audits/2026-10-02-finalisation.md
Normal file
38
docs/audits/2026-10-02-finalisation.md
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
# 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.
|
||||||
|
|
@ -60,3 +60,13 @@ Pour chaque sauvegarde, distinguer quatre preuves : **destination mentionnée**,
|
||||||
4. Rôle et persistance des swaps supplémentaires, ainsi que destination vérifiable des copies hors SSD.
|
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.
|
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,81 +1,280 @@
|
||||||
# Réinstallation — trame partielle
|
# Réinstallation d’ArchASP — procédure conditionnelle
|
||||||
|
|
||||||
> **État au 2 octobre 2026 : procédure partielle, non éprouvée sur un SSD
|
Cette procédure est un **parcours sur papier** : aucun SSD neuf, premier
|
||||||
> vierge dans ce lot.** Avant de l’employer, résoudre les décisions et
|
redémarrage, retour de snapshot ou restauration externe n’a été essayé dans ce
|
||||||
> dépendances de la [référence de reconstruction](reconstruction-baseline.md),
|
lot. Lire la [référence de reconstruction](reconstruction-baseline.md) et le
|
||||||
> vérifier une copie indépendante du dépôt, des dotfiles, des données et des
|
[bilan de finalisation](audits/2026-10-02-finalisation.md). Les commandes
|
||||||
> moyens de récupération Secure Boot. Les étapes Limine, signature, Snapper et
|
marquées « live » s’exécutent depuis un support Arch UEFI vérifié ; celles
|
||||||
> restauration des données ci-dessous ne forment pas encore une séquence
|
marquées « chroot » dans `arch-chroot /mnt` ; celles marquées « utilisateur »
|
||||||
> complète validée jusqu’au premier démarrage.
|
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
|
||||||
|
|
||||||
Depuis l’ISO Arch :
|
**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 :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ./scripts/prepare-disk.sh /dev/nvme0n1
|
lsblk -e7 -o PATH,TYPE,MAJ:MIN,SIZE,MODEL,SERIAL,WWN,FSTYPE,MOUNTPOINTS
|
||||||
|
findmnt -o SOURCE,TARGET,FSTYPE
|
||||||
|
swapon --show
|
||||||
```
|
```
|
||||||
|
|
||||||
La commande est destructive et exige une confirmation explicite.
|
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
|
||||||
## 2. Installer la base
|
pas :
|
||||||
|
|
||||||
Exemple :
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pacstrap -K /mnt base linux-lts linux-firmware amd-ucode btrfs-progs networkmanager git sudo neovim
|
: "${DISK:?Renseigner le disque entier confirmé avant toute suite}"
|
||||||
|
./scripts/prepare-disk.sh --dry-run "$DISK"
|
||||||
```
|
```
|
||||||
|
|
||||||
Générer ensuite le `fstab` depuis les montages réels :
|
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 :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
genfstab -U /mnt >> /mnt/etc/fstab
|
: "${DISK:?Cible obligatoire}"
|
||||||
|
./scripts/prepare-disk.sh "$DISK"
|
||||||
```
|
```
|
||||||
|
|
||||||
Ne jamais recopier directement l’ancien `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.
|
||||||
|
|
||||||
## 3. Configuration de base
|
## 4. Socle officiel — live, après montages vérifiés
|
||||||
|
|
||||||
Après `arch-chroot /mnt` :
|
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 :
|
||||||
- fuseau horaire, locale et hostname ;
|
|
||||||
- utilisateur et sudo ;
|
|
||||||
- réseau ;
|
|
||||||
- Limine ;
|
|
||||||
- initramfs ;
|
|
||||||
- Secure Boot ;
|
|
||||||
- Snapper.
|
|
||||||
|
|
||||||
## 4. Cloner le dépôt
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://git.labfytools.com/fy59/arch-system
|
pacman -Si base linux-lts linux-firmware amd-ucode btrfs-progs dosfstools \
|
||||||
cd arch-system
|
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
|
||||||
```
|
```
|
||||||
|
|
||||||
## 5. Restaurer par étapes
|
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.
|
||||||
|
|
||||||
`--packages` requiert `yay` pour la liste AUR, alors que `yay` figure lui-même
|
## 5. `fstab` — live, depuis les montages réels
|
||||||
dans cette liste : sur une installation neuve, cette partie est reportée.
|
|
||||||
L’amorçage vérifié de l’outil AUR et le choix des paquets étrangers nécessaires
|
Générer d’abord un candidat, le relire puis **remplacer** un nouveau fichier,
|
||||||
restent à documenter. Le noyau et le premier démarrage doivent être obtenus
|
jamais `>>` sur un `fstab` existant :
|
||||||
avec le socle disponible avant cette intégration facultative.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/deploy.sh --packages
|
genfstab -U /mnt > /mnt/etc/fstab.candidate
|
||||||
./scripts/deploy.sh --configs
|
nvim /mnt/etc/fstab.candidate
|
||||||
./scripts/deploy.sh --tools
|
findmnt -R /mnt
|
||||||
./scripts/deploy.sh --services
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. Finaliser manuellement
|
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.
|
||||||
|
|
||||||
- adapter `templates/fstab.btrfs` aux nouveaux UUID ;
|
## 6. Chroot et identité — live puis chroot root
|
||||||
- adapter `templates/limine.conf` à l’UUID Btrfs ;
|
|
||||||
- lancer `mkinitcpio -P` ;
|
Depuis le live, rendre le dépôt de reconstruction disponible dans le nouveau
|
||||||
- installer et configurer Limine ;
|
système à partir de la copie indépendante **déjà vérifiée** ; ne pas présumer
|
||||||
- créer la configuration Snapper ;
|
qu’un remote sera accessible :
|
||||||
- lancer limine-snapper-sync ;
|
|
||||||
- enrôler puis signer avec `sbctl` ;
|
```bash
|
||||||
- restaurer les dotfiles séparément ;
|
: "${REPO_SOURCE:?Chemin de la copie indépendante vérifiée obligatoire}"
|
||||||
- tester `nftables`, SSH, audio, veille et seuil de batterie avant le premier usage normal.
|
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).
|
||||||
|
|
|
||||||
|
|
@ -13,3 +13,23 @@ Le dépôt est public. Ne jamais y stocker :
|
||||||
- sauvegardes brutes de `/root`.
|
- 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.
|
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,4 +1,9 @@
|
||||||
# Migration du SSD 512 Go vers 1 To
|
# 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.
|
||||||
|
|
||||||
## Support de secours
|
## Support de secours
|
||||||
|
|
||||||
|
|
@ -8,7 +13,10 @@ Carte SD Ventoy contenant :
|
||||||
- Clonezilla Live ;
|
- Clonezilla Live ;
|
||||||
- GParted Live.
|
- GParted Live.
|
||||||
|
|
||||||
Avec des clés Secure Boot personnelles, désactiver temporairement Secure Boot pour démarrer Ventoy sans supprimer les clés du firmware.
|
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.
|
||||||
|
|
||||||
## Avant le clonage
|
## Avant le clonage
|
||||||
|
|
||||||
|
|
@ -22,7 +30,8 @@ Commandes de contrôle :
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
lsblk -p -o NAME,MODEL,SIZE,TRAN,FSTYPE,MOUNTPOINTS
|
lsblk -p -o NAME,MODEL,SIZE,TRAN,FSTYPE,MOUNTPOINTS
|
||||||
sudo smartctl -a /dev/nvme0n1
|
: "${SOURCE_DISK:?Identifier et vérifier la source avant lecture SMART}"
|
||||||
|
smartctl -a "$SOURCE_DISK"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Clonezilla
|
## Clonezilla
|
||||||
|
|
@ -35,7 +44,8 @@ Beginner
|
||||||
disk_to_local_disk
|
disk_to_local_disk
|
||||||
```
|
```
|
||||||
|
|
||||||
Source : Samsung 512 Go. Cible : Fanxiang 1 To.
|
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.
|
||||||
|
|
||||||
La cible est entièrement effacée.
|
La cible est entièrement effacée.
|
||||||
|
|
||||||
|
|
@ -57,3 +67,12 @@ sudo btrfs filesystem usage /
|
||||||
```
|
```
|
||||||
|
|
||||||
Garder le Samsung intact plusieurs jours avant de le reformater comme SSD externe.
|
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,55 +1,65 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Diagnostic de lecture seule. Les échecs de lecture restent visibles.
|
||||||
set -u
|
set -uo pipefail
|
||||||
|
status=0
|
||||||
section() { printf '\n=== %s ===\n' "$1"; }
|
section() { printf '\n=== %s ===\n' "$1"; }
|
||||||
|
read_command() {
|
||||||
section "CHARGE"
|
if ! "$@"; then printf '[!] Lecture échouée : %s\n' "$1" >&2; status=1; fi
|
||||||
uptime
|
}
|
||||||
|
top_processes() {
|
||||||
section "MÉMOIRE"
|
if ! ps -eo pid,comm,%cpu,%mem,rss "--sort=$1" | sed -n '1,20p'; then
|
||||||
free -h
|
printf '[!] Lecture échouée : ps (%s)\n' "$1" >&2
|
||||||
swapon --show
|
status=1
|
||||||
|
fi
|
||||||
section "PRESSION"
|
}
|
||||||
for file in /proc/pressure/{cpu,memory,io}; do
|
service_state() {
|
||||||
printf '%s\n' "--- $file"
|
local scope="$1" unit="$2" enabled active rc=0
|
||||||
cat "$file"
|
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"
|
||||||
done
|
done
|
||||||
|
section DISQUE
|
||||||
section "DISQUE"
|
read_command df -h /
|
||||||
df -h /
|
# Aucun prompt : la lecture privilégiée refusée est rapportée, jamais contournée.
|
||||||
sudo btrfs filesystem usage /
|
read_command sudo -n btrfs filesystem usage /
|
||||||
|
section 'TOP CPU'; top_processes -%cpu
|
||||||
section "TOP CPU"
|
section 'TOP RAM'; top_processes -rss
|
||||||
ps -eo pid,comm,%cpu,%mem,rss --sort=-%cpu | head -n 20
|
section 'SERVICES EN ÉCHEC'
|
||||||
|
read_command systemctl --failed
|
||||||
section "TOP RAM"
|
read_command systemctl --user --failed
|
||||||
ps -eo pid,comm,%cpu,%mem,rss --sort=-rss | head -n 20
|
section 'SERVICES À LA DEMANDE'
|
||||||
|
for unit in vmware-networks.service vmware-usbarbitrator.service smb.service nmb.service; do service_state system "$unit"; done
|
||||||
section "SERVICES EN ÉCHEC"
|
service_state user protonmail-bridge.service
|
||||||
systemctl --failed
|
section 'PROCESSUS OBSERVÉS (qualification manuelle)'
|
||||||
systemctl --user --failed
|
if pgrep -a -f 'nextcloud|thunderbird|protonmail-bridge|/protonmail/bridge|proton-pass|ollama|llama-server'; then
|
||||||
|
:
|
||||||
section "SERVICES À LA DEMANDE"
|
else
|
||||||
for unit in vmware-networks.service vmware-usbarbitrator.service smb.service nmb.service; do
|
rc=$?
|
||||||
printf '%-36s enabled=%-10s active=%s\n' \
|
if (( rc == 1 )); then echo 'Aucun processus correspondant observé.'
|
||||||
"$unit" \
|
else printf '[!] Recherche de processus échouée (%s).\n' "$rc" >&2; status=1; fi
|
||||||
"$(systemctl is-enabled "$unit" 2>/dev/null || true)" \
|
fi
|
||||||
"$(systemctl is-active "$unit" 2>/dev/null || true)"
|
section BATTERIE
|
||||||
done
|
if [[ -r /sys/class/power_supply/BAT1/charge_control_end_threshold ]]; then
|
||||||
printf '%-36s enabled=%-10s active=%s\n' \
|
read_command cat /sys/class/power_supply/BAT1/charge_control_end_threshold
|
||||||
protonmail-bridge.service \
|
else
|
||||||
"$(systemctl --user is-enabled protonmail-bridge.service 2>/dev/null || true)" \
|
echo 'Seuil BAT1 indisponible ou illisible ; état non établi.'
|
||||||
"$(systemctl --user is-active protonmail-bridge.service 2>/dev/null || true)"
|
fi
|
||||||
|
section DÉMARRAGE
|
||||||
section "PROCESSUS INDÉSIRABLES AU REPOS"
|
read_command systemd-analyze
|
||||||
pgrep -a -f 'nextcloud|thunderbird|protonmail-bridge|/protonmail/bridge|proton-pass|ollama|llama-server' \
|
read_command systemd-analyze blame
|
||||||
|| echo "Aucun"
|
exit "$status"
|
||||||
|
|
||||||
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
|
|
||||||
|
|
|
||||||
53
scripts/check-repo-data.py
Executable file
53
scripts/check-repo-data.py
Executable file
|
|
@ -0,0 +1,53 @@
|
||||||
|
#!/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,121 +1,31 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# Contrat : contrôle local, sans collecte, privilège ni mutation du poste.
|
||||||
set -Eeuo pipefail
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||||
readonly REPO_DIR="$(
|
|
||||||
cd "$(dirname "${BASH_SOURCE[0]}")/.."
|
|
||||||
pwd
|
|
||||||
)"
|
|
||||||
|
|
||||||
status=0
|
status=0
|
||||||
|
shopt -s nullglob
|
||||||
fail() {
|
bash_files=("$ROOT"/scripts/*.sh "$ROOT"/tests/*.sh "$ROOT"/tests/*/*.sh "$ROOT"/rootfs/usr/local/sbin/*)
|
||||||
printf '[!] %s\n' "$*" >&2
|
python_files=("$ROOT"/scripts/*.py "$ROOT"/tests/*/*.py)
|
||||||
status=1
|
(( ${#bash_files[@]} >= 10 && ${#python_files[@]} >= 2 )) || {
|
||||||
|
echo '[!] Scripts attendus absents.' >&2; exit 1;
|
||||||
}
|
}
|
||||||
|
for file in "${bash_files[@]}"; do
|
||||||
echo '[*] Validation syntaxique Bash'
|
[[ -f "$file" && -r "$file" ]] && bash -n "$file" || status=1
|
||||||
|
|
||||||
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
|
done
|
||||||
|
for file in "${python_files[@]}"; do
|
||||||
if [[ -f "$REPO_DIR/pkglist/pacman.txt" &&
|
[[ -f "$file" && -r "$file" ]] || { status=1; continue; }
|
||||||
-f "$REPO_DIR/pkglist/aur.txt" ]]; then
|
python3 - "$file" <<'PY' || status=1
|
||||||
if LC_ALL=C comm -12 \
|
import ast,sys
|
||||||
"$REPO_DIR/pkglist/pacman.txt" \
|
try: ast.parse(open(sys.argv[1],encoding='utf-8').read(),filename=sys.argv[1])
|
||||||
"$REPO_DIR/pkglist/aur.txt" |
|
except (SyntaxError,OSError) as exc: print(exc,file=sys.stderr); sys.exit(1)
|
||||||
grep -q .
|
PY
|
||||||
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
|
done
|
||||||
|
python3 "$ROOT/scripts/check-repo-data.py" "$ROOT" || status=1
|
||||||
echo '[*] Vérification des templates'
|
git -C "$ROOT" diff --no-ext-diff --check || status=1
|
||||||
|
git -C "$ROOT" diff --cached --check || status=1
|
||||||
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
|
if (( status == 0 )); then
|
||||||
echo '[+] Dépôt valide.'
|
echo '[+] Syntaxe et données du dépôt contrôlées ; aucun système installé ni démarré.'
|
||||||
else
|
else
|
||||||
echo '[!] Le dépôt contient encore des erreurs.' >&2
|
echo '[!] Contrôle du dépôt échoué.' >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit "$status"
|
exit "$status"
|
||||||
|
|
|
||||||
354
scripts/prepare-disk.py
Executable file
354
scripts/prepare-disk.py
Executable file
|
|
@ -0,0 +1,354 @@
|
||||||
|
#!/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,70 +1,5 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
# WHY: keep the public entry point while the topology checks parse structured data.
|
||||||
set -Eeuo pipefail
|
# CONTRACT: Python must be available on the live medium; no fallback to unchecked shell parsing.
|
||||||
|
set -euo pipefail
|
||||||
if (( EUID != 0 )); then
|
exec python3 "$(dirname "${BASH_SOURCE[0]}")/prepare-disk.py" "$@"
|
||||||
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"
|
|
||||||
|
|
|
||||||
22
tests/audit/mock-command.sh
Executable file
22
tests/audit/mock-command.sh
Executable file
|
|
@ -0,0 +1,22 @@
|
||||||
|
#!/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
|
||||||
43
tests/audit/test.sh
Executable file
43
tests/audit/test.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
||||||
|
#!/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'
|
||||||
136
tests/prepare-disk/mock-command.py
Executable file
136
tests/prepare-disk/mock-command.py
Executable file
|
|
@ -0,0 +1,136 @@
|
||||||
|
#!/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')
|
||||||
132
tests/prepare-disk/test.sh
Executable file
132
tests/prepare-disk/test.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
||||||
|
#!/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
Executable file
16
tests/run.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/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