Le nom par defaut etait fixe : le second export butait sur le premier. Il porte maintenant la date (l heure en plus le meme jour) ; un export de voutes identiques a la derniere archive n ecrit rien. Le LISEZ-MOI et la doc restaurent la plus recente archive. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
141 lines
6.1 KiB
Python
141 lines
6.1 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>" # -> setops-voutes-<date>-<poste>.tar
|
|
"""
|
|
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 _archive_libre, _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 _derniere_archive(dest: Path, nom: str) -> Path | None:
|
|
"""La plus recente archive de CE poste sur le support (datee ou non)."""
|
|
poste = os.uname().nodename
|
|
candidates = [a for a in dest.glob(f"{nom}-*{poste}.tar") if a.is_file()]
|
|
return max(candidates, key=lambda a: a.stat().st_mtime, default=None)
|
|
|
|
|
|
def _contenu(archive: Path) -> dict[str, str] | None:
|
|
"""{chemin: empreinte} de chaque voute de l'archive ; None si elle ne se lit pas."""
|
|
try:
|
|
with tarfile.open(archive, "r") as tar:
|
|
return {m.name: hashlib.sha256(tar.extractfile(m).read()).hexdigest()
|
|
for m in tar.getmembers() if m.isfile()}
|
|
except (OSError, tarfile.TarError):
|
|
return None
|
|
|
|
|
|
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 ?).")
|
|
attendu = {str(f.relative_to(DEPOTS)): _empreinte(f) for f in voutes}
|
|
|
|
# RIEN DE NOUVEAU, RIEN D'ECRIT (2026-10-04). Si la derniere archive de ce poste sur le
|
|
# support porte exactement ces voutes, une copie de plus n'ajoute rien.
|
|
precedente = _derniere_archive(dest, args.nom)
|
|
if precedente is not None and _contenu(precedente) == attendu:
|
|
print(f"\n A JOUR : {precedente.name} porte deja ces {len(attendu)} voute(s), "
|
|
"empreinte pour empreinte.\n Rien n'a ete ecrit.\n")
|
|
return 0
|
|
archive = _archive_libre(dest, args.nom, ".tar")
|
|
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:]))
|