Set-OPS-Public/scripts/exporter_voutes.py
Daniel Allaire 4855cda294 voutes : elles sortent du poste, dans leur propre archive, relues
exporter_voutes.py et make voutes-exporter : les six voutes (gitignorees, sur
aucune forge) sont sur la cle USB, a cote des cles ; restauration eprouvee.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 12:53:14 -04:00

119 lines
5 KiB
Python

#!/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-<date>-<poste>.tar -C "<dossier des depots>"
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/<support>" --nom setops-voutes-<date>
"""
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 \"<dossier des depots>\"")
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:]))