From 377462b1416331f31dbdc1684953acbc8a006121 Mon Sep 17 00:00:00 2001 From: Daniel Allaire Date: Fri, 28 Aug 2026 14:23:33 -0400 Subject: [PATCH] =?UTF-8?q?voutes=20:=20une=20voute,=20une=20cle=20?= =?UTF-8?q?=E2=80=94=20separer=20avant=20de=20distribuer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Decision de l'exploitant : chaque runner est maitre de sa voute et en detient la cle. C'est ce qui rend un runner autonome, donc ce qui rend l'emancipation atteignable. Elle exigeait un prealable, mesure ce matin : UN SEUL mot de passe ouvrait les SIX voutes de la flotte, celle de la fabric comprise. DISTRIBUER AVANT DE SEPARER AURAIT ETE PIRE QUE LE STATU QUO : poser « la » cle sur chaque runner rendait chaque runner capable d'ouvrir les autres. Compromettre le plus petit locataire donnait les secrets de l'hebergeur. Cinq cles, une par ecosysteme, sous ~/.config/setops-vault-. Verification croisee apres rechiffrement : la diagonale, et rien qu'elle. L'ancienne cle maitresse n'ouvre plus aucune des six. UN SEUL MOT DE PASSE NE POUVAIT PLUS SUFFIRE, et pas pour la raison qu'on croit : `cloner_vm_debian.yml` charge la voute du TENANT puis celle de l'UNDERLAY dans la meme execution. `ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes — un seul export suffit pour les 28 appels a ansible-playbook, sans en toucher un seul. La liste se derive dans scripts/voutes.py. CE QUI BORNE LE POUVOIR N'EST PAS LA LISTE MAIS LA PRESENCE DES FICHIERS. Sur le poste, toutes les cles sont la — c'est l'humain qui les detient toutes, et P03 lit les inventaires de tous les freres. Sur un runner, une seule existe. Le code est identique, le pouvoir ne l'est pas. DEUX PREUVES ONT DIT CE QUI MANQUAIT. P03 est tombee des la separation : elle lit les inventaires voisins, donc il lui faut leurs cles — c'est elle qui a etabli que la liste devait couvrir le voisinage. P16 s'est mise a SAUTER : sa garde ne connaissait que ANSIBLE_VAULT_PASSWORD_FILE. Une preuve sautee se lit trop facilement comme une preuve passee. COUT ASSUME : le chiffre et sa cle cohabiteront sur la meme machine des que les runners recevront la leur. Le pari tient parce que le perimetre est borne. Les six voutes sauvegardees avant rechiffrement. L'ancienne cle maitresse reste sur le poste : la retirer est une decision, pas un nettoyage. make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans ANSIBLE_VAULT_PASSWORD_FILE. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on --- CHANGELOG.md | 72 ++++++++++++++++ Makefile | 24 +++++- docs/audit/preuve-2026-08-28.md | 6 +- scripts/prouver.py | 11 ++- scripts/voutes.py | 142 ++++++++++++++++++++++++++++++++ 5 files changed, 246 insertions(+), 9 deletions(-) create mode 100755 scripts/voutes.py diff --git a/CHANGELOG.md b/CHANGELOG.md index a04c1df..f1911c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # CHANGELOG — Set-OPS +## 2026-08-28 — Une voûte, une clé : la séparation avant la distribution + +**52 preuves.** Décision de l'exploitant : **chaque runner est maître de sa voûte et en +détient la clé.** C'est ce qui rend un runner autonome — et donc ce qui rend l'émancipation +atteignable. Mais elle exigeait un préalable, mesuré ce matin : + +``` +UN SEUL mot de passe ouvrait les SIX voûtes de la flotte + SITE-Chezlepro (la fabric) · OPS-Chezlepro · OPS-Chezlepro-lab (×2) + OPS-Patient0 · OPS-Technolibre +``` + +**Distribuer avant de séparer aurait été strictement pire que le statu quo.** Poser « la » +clé sur chaque runner, c'était rendre chaque runner capable d'ouvrir les autres : +compromettre le plus petit locataire donnait les secrets de l'hébergeur. *L'ordre n'est pas +un détail de mise en œuvre, c'est ce qui décide si la mesure protège ou expose.* + +Cinq clés, une par écosystème, sous `~/.config/setops-vault-`. +Vérification croisée après rechiffrement — la diagonale, et rien qu'elle : + +``` +voûte \ clé chezlepro lab patient0 site technolibre ANCIENNE +ops-chezlepro OUVRE . . . . refusée +ops-chezlepro-lab . OUVRE . . . refusée +ops-patient0 . . OUVRE . . refusée +site-chezlepro . . . OUVRE . refusée +ops-technolibre . . . . OUVRE refusée +``` + +### Un seul mot de passe ne pouvait plus suffire, et pas pour la raison qu'on croit + +`cloner_vm_debian.yml` charge **deux voûtes dans la même exécution** : celle du tenant, +puis celle de l'underlay — les identifiants Proxmox appartiennent à l'hébergeur, le reste +au locataire. `ANSIBLE_VAULT_PASSWORD_FILE` n'en porte qu'une. + +`ANSIBLE_VAULT_IDENTITY_LIST` en porte plusieurs et les essaie toutes sur un bloc chiffré +sans étiquette. **Un seul `export` dans le Makefile suffit donc pour les 28 appels à +`ansible-playbook`, sans en toucher un seul.** La liste se dérive de l'instance montée, de +son site et des dépôts frères (`scripts/voutes.py`). + +*Ce qui borne le pouvoir n'est pas cette liste mais la **présence** des fichiers.* Sur le +poste de l'exploitant, toutes les clés sont là — c'est l'humain qui les détient toutes, et +des preuves comme P03 lisent les inventaires de tous les écosystèmes frères. Sur un runner, +une seule clé existe, et la même fonction n'en nomme qu'une. **Le code est identique, le +pouvoir ne l'est pas.** + +### Deux preuves ont immédiatement dit ce qui manquait + +**P03** (diff-vide de *toutes* les instances) est tombée dès la séparation : elle lit les +inventaires des écosystèmes voisins, donc il lui faut leurs clés. C'est elle qui a établi +que la liste devait couvrir le voisinage sur le poste — je l'avais d'abord limitée à +l'instance montée et à son site. + +**P16** s'est mise à **sauter** : sa garde ne connaissait que `ANSIBLE_VAULT_PASSWORD_FILE`. +*Une preuve sautée se lit beaucoup trop facilement comme une preuve passée* — c'est le +même défaut que le vert sur un périmètre vide, en plus discret. + +### Ce que ça coûte, et qui est assumé + +Le chiffré et sa clé cohabiteront sur la même machine dès que les runners recevront la +leur. Voler le disque d'un runner suffira, là où ça ne donnait rien. Le pari tient parce +que le périmètre est borné : **le runner d'un tenant ne peut ouvrir que ce tenant.** Trois +contreparties en découlent — mode `0600`, la clé hors du chemin de sauvegarde ordinaire, et +un runner durci comme un détenteur d'état. + +*Les six voûtes ont été sauvegardées telles quelles avant rechiffrement. L'ancienne clé +maîtresse n'ouvre plus rien et reste sur le poste : la retirer est une décision, pas un +nettoyage.* + +make verifier : vert. make prouver : CONFORME, 52 OK, 0 echec, 0 saute — sans +`ANSIBLE_VAULT_PASSWORD_FILE`. + ## 2026-08-28 — L'insémination aboutit, et trois tests qui ne gardaient rien **52 preuves.** Le flux était déclaré et appliqué ; il manquait l'**identité**. Le runner diff --git a/Makefile b/Makefile index 8b0afdc..2fe6874 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,22 @@ export ANSIBLE_SSH_CONTROL_PATH_DIR ?= $(CURDIR)/.ansible/cp export ANSIBLE_SSH_ARGS ?= -F /dev/null -o ControlMaster=no export SETOPS_INSTANCE ?= instance + +# UNE VOUTE, UNE CLE — ET ANSIBLE LES TIENT TOUTES A LA FOIS (2026-08-28). +# +# Un seul mot de passe ouvrait les six voutes de la flotte, celle de la fabric comprise. +# Depuis la separation, chaque ecosysteme a la sienne, sous +# `~/.config/setops-vault-`. +# +# UN `ANSIBLE_VAULT_PASSWORD_FILE` NE PEUT PLUS SUFFIRE : `cloner_vm_debian.yml` charge +# la voute du TENANT puis celle de l'UNDERLAY dans la meme execution — les identifiants +# Proxmox appartiennent a l'hebergeur, le reste au locataire. `ANSIBLE_VAULT_IDENTITY_LIST` +# porte plusieurs secrets et les essaie tous ; un seul export suffit donc pour les 28 +# appels a `ansible-playbook`, sans en toucher un seul. +# +# `?=` : une valeur deja posee dans l'environnement gagne — la GUI et les runners peuvent +# nommer leurs propres cles sans que ce fichier ait a les connaitre. +export ANSIBLE_VAULT_IDENTITY_LIST ?= $(shell python3 scripts/voutes.py identites 2>/dev/null) # Inventaire de l'instance : un seul par instance dans le modèle « séparation par # instance ». Détection rétro-compatible : principal > production > lab. # Surchargeable : make … SETOPS_INVENTAIRE=chemin/hosts.yml @@ -636,7 +652,7 @@ deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des cou python3 scripts/orchestrer.py verifier; \ python3 scripts/orchestrer.py ecrire; \ vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ - if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then \ + if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ @@ -685,7 +701,7 @@ deployer-tout: _instance-requise ## Deploie TOUTE la flotte dans l'ordre des cou # clairement plutot que de pendre sur une invite que personne ne lit. define VAULT_UNE_FOIS vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ - if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then \ + if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible (demande une seule fois): ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ @@ -928,7 +944,7 @@ verifier-deploiement: ansible-runtime ## Verifie l'etat de la flotte apres deplo exit 2; \ fi; \ vault_chiffre="$$(grep -rlsIF '$$ANSIBLE_VAULT' $(dir $(INVENTAIRE_PRODUCTION))group_vars 2>/dev/null | head -1 || true)"; \ - if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then \ + if [[ -n "$$vault_chiffre" && -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ if [[ -t 0 ]]; then \ read -r -s -p 'Mot de passe du vault Ansible: ' mdp; echo; \ vf="$$(mktemp)"; printf '%s' "$$mdp" > "$$vf"; chmod 600 "$$vf"; \ @@ -1010,7 +1026,7 @@ cloner-vm: ansible-runtime ## Clone une VM depuis le gabarit dore — HOTE= read -r premiere_ligne < "$$vault_file" || true; \ case "$$premiere_ligne" in \ '$$ANSIBLE_VAULT'*) \ - if [[ -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}" ]]; then \ + if [[ -z "$${ANSIBLE_VAULT_PASSWORD_FILE:-}$${ANSIBLE_VAULT_IDENTITY_LIST:-}" ]]; then \ vault_args+=( --ask-vault-pass ); \ fi; \ ;; \ diff --git a/docs/audit/preuve-2026-08-28.md b/docs/audit/preuve-2026-08-28.md index 62339f5..919716a 100644 --- a/docs/audit/preuve-2026-08-28.md +++ b/docs/audit/preuve-2026-08-28.md @@ -30,7 +30,7 @@ | P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | | P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 15 hotes, 36 groupes (inventaire dechiffre et parse). | | P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | -| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. Voute reelle : 25 cle(s), aucun manque. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 23 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) | | P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | | P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | | P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 3 instance(s) federee(s), aucun index en collision. | @@ -43,7 +43,7 @@ | P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 3 pool(s) Proxmox, 35 VM placee(s), aucun nom ni VMID en collision. | | P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 29 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 18, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | | P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. | -| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 51 scripts expliques et atteignables, 104 cibles make documentees, 61 roles avec README. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 52 scripts expliques et atteignables, 104 cibles make documentees, 61 roles avec README. | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 40 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 33 revendication(s) de port, aucune collision entre roles co-localises (38 groupes). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (26 genere(s) exempte(s)). | @@ -53,7 +53,7 @@ | P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 36 role(s) serveur/client tous nommes, 37 groupe(s) cite(s) en table existent tous. | | P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 27 page(s) de wiki toutes atteignables. | | P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. | -| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 47 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | +| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 48 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). | | P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 4 edge(s) emettent un certificat portant les noms publies (OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre/principal, OPS-Patient0/product | | P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 5 machine(s) du plan retrouvees, 57 regle(s) du site. | | P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. | diff --git a/scripts/prouver.py b/scripts/prouver.py index 1d45ad0..a9ce337 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -2014,8 +2014,15 @@ PREUVES: list[dict] = [ def _vault_requis_absent() -> bool: - """Vrai si l'inventaire chiffre un group_vars et qu'aucun mot de passe n'est fourni.""" - if os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE"): + """Vrai si l'inventaire chiffre un group_vars et qu'aucun mot de passe n'est fourni. + + DEUX FACONS DE FOURNIR UNE CLE DEPUIS LA SEPARATION DES VOUTES (2026-08-28) : l'unique + `ANSIBLE_VAULT_PASSWORD_FILE`, et `ANSIBLE_VAULT_IDENTITY_LIST`, qui en porte + plusieurs — indispensable des qu'une execution touche deux voutes (le tenant et + l'underlay). N'en regarder qu'une faisait SAUTER P16 alors que la cle etait la, et + une preuve sautee se lit trop facilement comme une preuve passee. + """ + if os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") or os.environ.get("ANSIBLE_VAULT_IDENTITY_LIST"): return False gv = (RACINE / INV).parent / "group_vars" if not gv.exists(): diff --git a/scripts/voutes.py b/scripts/voutes.py new file mode 100755 index 0000000..cc21c40 --- /dev/null +++ b/scripts/voutes.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Les voutes de cet ecosysteme, et la cle de chacune. + +UNE VOUTE, UNE CLE (2026-08-28). Un seul mot de passe ouvrait les six voutes de la +flotte — celle de la fabric comprise. Tant que c'etait vrai, poser « la » cle sur chaque +runner aurait rendu chaque runner capable d'ouvrir les autres : compromettre le plus +petit locataire, c'etait obtenir les secrets de l'hebergeur. La separation precede donc +la distribution, et cet ordre n'est pas negociable. + +CE FICHIER NE LIT AUCUN SECRET. Il ne fait que dire OU chaque cle se trouve, et laisse +Ansible l'ouvrir. Les chemins sont des pointeurs, jamais des valeurs. + +DEUX VOUTES DANS LA MEME EXECUTION, ET C'EST LE CAS COURANT : `cloner_vm_debian.yml` +charge celle du tenant PUIS celle de l'underlay — les identifiants Proxmox appartiennent +a l'hebergeur, le reste au locataire. Un unique `ANSIBLE_VAULT_PASSWORD_FILE` ne peut +donc plus suffire. Ansible sait tenir plusieurs secrets a la fois +(`ANSIBLE_VAULT_IDENTITY_LIST`) et les essaie tous sur un bloc chiffre sans etiquette : +c'est exactement ce qu'il nous faut, et ca n'oblige a toucher aucun des 28 appels. + +CONVENTION : ~/.config/setops-vault- + OPS-Chezlepro -> ~/.config/setops-vault-ops-chezlepro + SITE-Chezlepro -> ~/.config/setops-vault-site-chezlepro + +Usage : + python3 scripts/voutes.py identites # la liste pour ANSIBLE_VAULT_IDENTITY_LIST + python3 scripts/voutes.py etat # ce qui est present, ce qui manque +""" +from __future__ import annotations + +import argparse +import os +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) + +from inventory_rules import instance_courante # noqa: E402 +import underlay as underlay_mod # noqa: E402 + +PREFIXE = "setops-vault-" + + +def _config() -> Path: + """Le dossier des cles. `XDG_CONFIG_HOME` respecte, `~/.config` par defaut.""" + return Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config")) + + +def slug(nom: str) -> str: + return nom.strip().lower() + + +def cle_de(nom_depot: str) -> Path: + return _config() / f"{PREFIXE}{slug(nom_depot)}" + + +def _freres() -> list[str]: + """Les depots d'ecosysteme poses a cote du moteur — ceux qui portent un `plan/`. + + MEME DECOUVERTE QUE LE RESTE DU DEPOT : des dossiers freres, pas de registre. + """ + racine = Path(__file__).resolve().parents[1] + return sorted(d.name for d in racine.parent.iterdir() + if d.is_dir() and (d / "plan").is_dir()) + + +def voutes() -> list[tuple[str, str, Path]]: + """[(role, nom du depot, chemin de la cle)] — tout ce que CETTE machine peut ouvrir. + + L'ORDRE COMPTE PEU POUR ANSIBLE (il essaie tous les secrets) mais beaucoup pour un + humain qui lit la sortie : ce qu'on pilote, le terrain, puis le voisinage. + + POURQUOI LE VOISINAGE Y FIGURE, ET POURQUOI CE N'EST PAS UNE FUITE. La liste nomme + des CHEMINS, pas des secrets : une cle absente du disque n'est pas listee, et la + machine ne peut ouvrir que ce qu'elle detient. C'est donc la PRESENCE des fichiers + qui borne le pouvoir, jamais cette fonction. + + Sur le poste de l'exploitant, toutes les cles sont la, et c'est voulu : c'est + l'humain qui les detient toutes, et des preuves comme P03 lisent les inventaires de + TOUS les ecosystemes freres. Sur un runner, une seule cle existe — la sienne — et la + meme fonction n'en nomme qu'une. Le code est identique, le pouvoir ne l'est pas. + """ + out: list[tuple[str, str, Path]] = [] + vus: set[str] = set() + + def ajouter(role: str, nom: str) -> None: + if nom and nom not in vus: + vus.add(nom) + out.append((role, nom, cle_de(nom))) + + try: + inst = instance_courante() + if inst is not None: + ajouter("instance", Path(inst).resolve().name) + except Exception: + pass + c = underlay_mod.chemin() + if c is not None: + ajouter("hebergeur", c.resolve().parent.name) + for nom in _freres(): + ajouter("voisin", nom) + return out + + +def identites() -> str: + """`etiquette@chemin,...` pour ANSIBLE_VAULT_IDENTITY_LIST — les cles PRESENTES. + + Une cle absente est SAUTEE plutot que declaree : Ansible refuse de demarrer si un + fichier d'identite n'existe pas, et l'echec porterait alors sur une voute que + l'execution n'ouvre peut-etre meme pas. Ce qui manque se lit par `voutes.py etat`. + """ + return ",".join(f"{slug(nom)}@{chemin}" for _, nom, chemin in voutes() if chemin.is_file()) + + +def etat() -> int: + v = voutes() + if not v: + print("Aucune voute a nommer : ni instance montee, ni underlay.") + return 0 + manquantes = 0 + for role, nom, chemin in v: + present = chemin.is_file() + manquantes += 0 if present else 1 + print(f"{role:<11} {nom:<20} {'cle presente' if present else 'CLE ABSENTE '} {chemin}") + if manquantes: + print(f"\n{manquantes} cle(s) absente(s). En creer une :", + "umask 077; openssl rand -base64 48 | tr -d '\\n' > ", sep="\n ") + return 1 if manquantes else 0 + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("commande", choices=("identites", "etat")) + args = ap.parse_args(argv) + if args.commande == "identites": + print(identites()) + return 0 + return etat() + + +if __name__ == "__main__": + raise SystemExit(main())