exporter : archives datees, et rien d ecrit si les voutes n ont pas change
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>
This commit is contained in:
parent
1167cc12e0
commit
5d7d2a8918
4 changed files with 89 additions and 26 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
|
@ -1,5 +1,32 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-10-04 (75) — Exporter les voûtes une seconde fois ne bute plus sur la première
|
||||
|
||||
**Signalé par l'exploitant** : `make voutes-exporter VERS=<clé>` a refusé (« existe deja.
|
||||
On n'ecrase pas une sauvegarde »). Il a dû retirer l'ancienne archive à la main, puis relancer.
|
||||
|
||||
**Cause** : le nom par défaut était fixe (`setops-voutes-<poste>.tar`, et de même
|
||||
`setops-cles-<poste>.tar[.gpg]`). La docstring promettait `setops-voutes-<date>-<poste>`,
|
||||
mais le code ne datait pas. Les exports précédents n'avaient passé que parce qu'on datait
|
||||
soi-même par `NOM=`.
|
||||
|
||||
**Fait** (`scripts/exporter_cles.py`, `scripts/exporter_voutes.py`) :
|
||||
- le nom porte la date (`<nom>-AAAA-MM-JJ-<poste>`), et l'heure en plus au second export du
|
||||
même jour. Le refus de l'écrasement reste, en dernier recours ;
|
||||
- voûtes : si la dernière archive de ce poste sur le support porte exactement ces voûtes
|
||||
(mêmes chemins, mêmes empreintes), **rien n'est écrit** (« A JOUR »). Une copie de plus
|
||||
n'ajoute rien ;
|
||||
- le LISEZ-MOI déposé sur la clé et `docs/sortir-les-cles-du-poste.md` restaurent la **plus
|
||||
récente** archive (`ls -t … | head -1`). Leur `setops-voutes-*.tar`, avec plusieurs
|
||||
archives sur le support, passait plusieurs noms à `tar -xf`.
|
||||
|
||||
**Éprouvé** sur un support simulé (scratchpad, effacé ensuite) : premier export daté ;
|
||||
second export identique → « A JOUR », rien d'écrit ; une voûte changée (archive précédente
|
||||
altérée) → nouvelle archive horodatée à côté de la première.
|
||||
|
||||
Le LISEZ-MOI déjà sur la clé garde l'ancienne formule ; `make cles-compagnons
|
||||
VERS=<clé>` le remplace sans refaire l'archive.
|
||||
|
||||
## 2026-10-04 (74) — Un dépôt verrouillé n'est plus une panne
|
||||
|
||||
**Constaté en vérifiant (73)** : lancées ensemble sur `web-dorsal-01`, la vérification du
|
||||
|
|
|
|||
|
|
@ -15,7 +15,10 @@ ce poste et sur le runner de son écosystème, qui en reçoit une copie chiffré
|
|||
d'une seconde couche ; le script refuse tout fichier dont l'en-tête n'est pas
|
||||
`$ANSIBLE_VAULT`, garde le chemin de chaque voûte (elles s'appellent presque toutes
|
||||
`vault.yml`) et relit l'archive empreinte par empreinte. Restauration :
|
||||
`tar -xf setops-voutes-*.tar -C <dossier des dépôts>`. **À refaire après toute écriture
|
||||
`tar -xf "$(ls -t setops-voutes-*.tar | head -1)" -C <dossier des dépôts>` (la plus récente).
|
||||
Chaque export porte sa date (`setops-voutes-<date>-<poste>.tar`, l'heure en plus au second
|
||||
export du jour) et n'écrit rien si les voûtes n'ont pas changé depuis la dernière archive.
|
||||
**À refaire après toute écriture
|
||||
dans une voûte**, comme les clés après toute voûte nouvelle.
|
||||
|
||||
> **Ce chiffre était trois, et il a baissé sans que rien ne le signale.** Un coffre répliqué deux fois reste
|
||||
|
|
@ -77,7 +80,7 @@ Une procédure rangée dans le dépôt serait donc inaccessible exactement quand
|
|||
La clé se suffit donc à elle-même. À côté de l'archive :
|
||||
|
||||
```
|
||||
setops-cles-<poste>.tar.gpg les clés, chiffrées
|
||||
setops-cles-<date>-<poste>.tar.gpg les clés, chiffrées (une archive datée par export)
|
||||
restaurer_cles.py le script, autonome
|
||||
LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
|
||||
```
|
||||
|
|
@ -89,8 +92,9 @@ LISEZ-MOI-RESTAURATION.txt le mode d'emploi, et les commandes manuelles
|
|||
Sur la machine neuve, il ne faut que `gpg`, `python3` et la phrase de passe :
|
||||
|
||||
```bash
|
||||
python3 restaurer_cles.py setops-cles-*.tar.gpg --lister # voir sans rien écrire
|
||||
python3 restaurer_cles.py setops-cles-*.tar.gpg # remettre en place
|
||||
A="$(ls -t setops-cles-*.tar* | head -1)" # la plus récente
|
||||
python3 restaurer_cles.py "$A" --lister # voir sans rien écrire
|
||||
python3 restaurer_cles.py "$A" # remettre en place
|
||||
```
|
||||
|
||||
Le script remet chaque fichier à sa place selon son nom, et **repose les droits à 0600**.
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ qu'il vient d'ecrire et compare les empreintes, une a une.
|
|||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import hashlib
|
||||
import os
|
||||
import subprocess
|
||||
|
|
@ -94,6 +95,23 @@ def _refuser_destination_interne(dest: Path) -> None:
|
|||
" cle est a l'interieur. Viser un support amovible ou une machine tierce.")
|
||||
|
||||
|
||||
def _archive_libre(dest: Path, nom: str, ext: str) -> Path:
|
||||
"""Le nom de la NOUVELLE archive : `<nom>-<date>-<poste><ext>` (2026-10-04).
|
||||
|
||||
Le nom etait fixe (`<nom>-<poste>`) : le second export butait toujours sur le premier, et
|
||||
l'exploitant devait deplacer l'ancien a la main ou dater lui-meme par `NOM=`. Dater par
|
||||
defaut garde l'historique sur le support ; un second export le meme jour prend l'heure.
|
||||
On n'ecrase jamais : si meme ce nom-la existe, on refuse.
|
||||
"""
|
||||
poste = os.uname().nodename
|
||||
maintenant = dt.datetime.now()
|
||||
archive = dest / f"{nom}-{maintenant:%Y-%m-%d}-{poste}{ext}"
|
||||
if archive.exists():
|
||||
archive = dest / f"{nom}-{maintenant:%Y-%m-%d-%H%M%S}-{poste}{ext}"
|
||||
if archive.exists():
|
||||
raise SystemExit(f"REFUS : {archive} existe deja. On n'ecrase pas une sauvegarde.")
|
||||
return archive
|
||||
|
||||
LISEZ_MOI = """SI TU LIS CECI, LE POSTE EST PROBABLEMENT MORT.
|
||||
==================================================
|
||||
|
||||
|
|
@ -108,8 +126,10 @@ CE QU'IL FAUT, ET RIEN D'AUTRE
|
|||
|
||||
LA MANOEUVRE
|
||||
------------
|
||||
python3 restaurer_cles.py setops-cles-*.tar.gpg --lister # voir sans rien ecrire
|
||||
python3 restaurer_cles.py setops-cles-*.tar.gpg # remettre en place
|
||||
# Une archive par export, datee : prendre la PLUS RECENTE.
|
||||
A="$(ls -t setops-cles-*.tar* | head -1)"
|
||||
python3 restaurer_cles.py "$A" --lister # voir sans rien ecrire
|
||||
python3 restaurer_cles.py "$A" # remettre en place
|
||||
|
||||
Le script remet chaque fichier a sa place (~/.config ou ~/.ssh) et repose les droits a
|
||||
0600. Ce dernier point n'est pas cosmetique : `ssh` REFUSE une cle privee que d'autres
|
||||
|
|
@ -121,7 +141,7 @@ SI MEME CE SCRIPT NE TOURNE PAS
|
|||
-------------------------------
|
||||
Trois commandes suffisent. C'est le vrai filet :
|
||||
|
||||
gpg --output cles.tar --decrypt setops-cles-*.tar.gpg
|
||||
gpg --output cles.tar --decrypt "$(ls -t setops-cles-*.tar.gpg | head -1)"
|
||||
mkdir -p ~/.config ~/.ssh && tar -xf cles.tar -C /tmp
|
||||
cp /tmp/setops-vault-* ~/.config/ ; cp /tmp/id_* ~/.ssh/
|
||||
chmod 700 ~/.ssh ~/.config ; chmod 600 ~/.ssh/id_* ~/.config/setops-vault-*
|
||||
|
|
@ -131,7 +151,7 @@ LES VOUTES, DANS UNE AUTRE ARCHIVE
|
|||
Les cles ci-dessus OUVRENT les voutes ; les voutes elles-memes (chiffrees, gitignorees,
|
||||
donc sur aucune forge) sont dans `setops-voutes-*.tar`. Elles gardent leur chemin :
|
||||
|
||||
tar -xf setops-voutes-*.tar -C "<dossier ou sont clones les depots>"
|
||||
tar -xf "$(ls -t setops-voutes-*.tar | head -1)" -C "<dossier ou sont clones les depots>"
|
||||
|
||||
Sans elles, les cles n'ouvrent rien : chaque secret des ecosystemes serait a refaire.
|
||||
|
||||
|
|
@ -254,12 +274,7 @@ def main() -> int:
|
|||
# defaut reste l'archive GPG, parce qu'un support non chiffre est le cas le plus
|
||||
# frequent et que le silence ne doit jamais pencher du cote de la divulgation.
|
||||
if args.support_chiffre:
|
||||
archive = dest / f"{args.nom}-{os.uname().nodename}.tar"
|
||||
if archive.exists():
|
||||
raise SystemExit(
|
||||
f"REFUS : {archive} existe deja.\n"
|
||||
" On n'ecrase pas une sauvegarde de cles : elle est peut-etre la seule.\n"
|
||||
" La deplacer ou choisir un autre --nom.")
|
||||
archive = _archive_libre(dest, args.nom, ".tar")
|
||||
with tarfile.open(archive, "w") as tar:
|
||||
for f, _ in fichiers:
|
||||
tar.add(f, arcname=f.name)
|
||||
|
|
@ -281,12 +296,7 @@ def main() -> int:
|
|||
_deposer_compagnons(dest)
|
||||
return 0
|
||||
|
||||
archive = dest / f"{args.nom}-{os.uname().nodename}.tar.gpg"
|
||||
if archive.exists():
|
||||
raise SystemExit(
|
||||
f"REFUS : {archive} existe deja.\n"
|
||||
" On n'ecrase pas une sauvegarde de cles : elle est peut-etre la seule.\n"
|
||||
" La deplacer ou choisir un autre --nom.")
|
||||
archive = _archive_libre(dest, args.nom, ".tar.gpg")
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
brut = Path(tmp) / "cles.tar"
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ ECRIRE PUIS RELIRE (D-68) : l'archive est rouverte et l'empreinte de CHAQUE vout
|
|||
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>
|
||||
python3 scripts/exporter_voutes.py --vers "/media/<support>" # -> setops-voutes-<date>-<poste>.tar
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ import tarfile
|
|||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
from exporter_cles import _refuser_destination_interne # noqa: E402
|
||||
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")
|
||||
|
|
@ -46,6 +46,23 @@ 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)
|
||||
|
|
@ -81,11 +98,16 @@ def main(argv: list[str]) -> int:
|
|||
_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}
|
||||
|
||||
# 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)))
|
||||
|
|
|
|||
Loading…
Reference in a new issue