#!/usr/bin/env python3 """Sort les VOUTES du poste — la moitie du coffre que `exporter_cles.py` n'emporte pas. CE QUI MANQUAIT (mesure du 2026-09-28). La cle USB portait les CLES qui ouvrent les voutes, pas les voutes elles-memes. Or les voutes sont gitignorees : elles ne sont sur AUCUNE forge. Chacune vit sur ce poste et sur le runner de son ecosysteme, et nulle part ailleurs. Poste et runner perdus ensemble, les cles restauraient l'acces a... rien : tous les secrets de l'ecosysteme etaient a regenerer. UNE ARCHIVE A PART, PAS DANS CELLE DES CLES. `restaurer_cles.py` range chaque cle d'apres son seul NOM (`~/.config` ou `~/.ssh`). Les voutes s'appellent presque toutes `vault.yml` : elles s'y ecraseraient. Ici, chaque voute garde son chemin relatif aux depots — `OPS-Chezlepro/inventories/principal/group_vars/all/vault.yml` — et se restaure par : tar -xf setops-voutes--.tar -C "" PAS DE SECONDE COUCHE DE CHIFFREMENT, et c'est fonde : chaque voute est deja chiffree (AES256, `ansible-vault`) par une cle qui n'est PAS dans cette archive. Le script REFUSE donc tout fichier dont l'en-tete n'est pas `$ANSIBLE_VAULT` : une voute dechiffree par accident ne sortira jamais du poste par ce chemin. ECRIRE PUIS RELIRE (D-68) : l'archive est rouverte et l'empreinte de CHAQUE voute comparee a l'original. Au moindre ecart, elle est supprimee. python3 scripts/exporter_voutes.py --recenser python3 scripts/exporter_voutes.py --vers "/media/" --nom setops-voutes- """ from __future__ import annotations import argparse import hashlib import os import sys import tarfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) from exporter_cles import _refuser_destination_interne # noqa: E402 DEPOTS = Path(__file__).resolve().parents[2] MOTIFS = ("*/inventories/*/group_vars/**/*vault.yml", "*/*.vault.yml") ENTETE = b"$ANSIBLE_VAULT" def _empreinte(chemin: Path) -> str: return hashlib.sha256(chemin.read_bytes()).hexdigest() def recenser() -> list[Path]: trouves = {f for m in MOTIFS for f in DEPOTS.glob(m) if f.is_file()} return sorted(trouves) def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) ap.add_argument("--recenser", action="store_true", help="montrer, sans rien ecrire") ap.add_argument("--vers", help="repertoire de destination (support amovible)") ap.add_argument("--nom", default="setops-voutes", help="nom de base de l'archive") args = ap.parse_args(argv) voutes = recenser() en_clair = [f for f in voutes if not f.read_bytes().startswith(ENTETE)] print(f"\n LES VOUTES DE CE POSTE ({DEPOTS}) :\n") for f in voutes: etat = "chiffree" if f not in en_clair else "EN CLAIR — REFUSEE" print(f" {str(f.relative_to(DEPOTS)):<66} {f.stat().st_size:>6} o " f"{_empreinte(f)[:16]}… {etat}") print(f"\n {len(voutes)} voute(s).") if en_clair: raise SystemExit("\n REFUS : une voute n'est pas chiffree. Rien ne sort du poste tant " "qu'elle ne l'est pas.") if not voutes: raise SystemExit(" Aucune voute trouvee : rien a sortir.") if args.recenser: print("\n RECENSEMENT SEUL — rien n'a ete ecrit.\n") return 0 if not args.vers: raise SystemExit(" --vers est requis pour ecrire.") dest = Path(args.vers).expanduser() _refuser_destination_interne(dest) if not dest.is_dir(): raise SystemExit(f" REFUS : {dest} n'est pas un repertoire (support monte ?).") archive = dest / f"{args.nom}-{os.uname().nodename}.tar" if archive.exists(): raise SystemExit(f" REFUS : {archive} existe deja. On n'ecrase pas une sauvegarde.") attendu = {str(f.relative_to(DEPOTS)): _empreinte(f) for f in voutes} with tarfile.open(archive, "w") as tar: for f in voutes: tar.add(f, arcname=str(f.relative_to(DEPOTS))) archive.chmod(0o600) ecarts = [] with tarfile.open(archive, "r") as tar: noms = tar.getnames() for nom, emp in attendu.items(): membre = tar.extractfile(nom) if nom in noms else None if membre is None: ecarts.append(f"{nom} : ABSENT") elif hashlib.sha256(membre.read()).hexdigest() != emp: ecarts.append(f"{nom} : empreinte differente") ecarts += [f"{n} : EN TROP" for n in noms if n not in attendu] if ecarts: archive.unlink(missing_ok=True) print("\n ECARTS — l'archive a ete SUPPRIMEE :", file=sys.stderr) for e in ecarts: print(f" ! {e}", file=sys.stderr) return 1 print(f"\n Ecrit : {archive}") print(f" {len(attendu)} voute(s) relue(s), empreintes identiques a l'original.") print(f" Restaurer : tar -xf \"{archive.name}\" -C \"\"") print(" Les CLES qui les ouvrent sont dans l'archive `setops-cles-*` — pas dans celle-ci.\n") return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))