diff --git a/docs/audit/preuve-2026-08-27.md b/docs/audit/preuve-2026-08-27.md index 8aadbda..8230065 100644 --- a/docs/audit/preuve-2026-08-27.md +++ b/docs/audit/preuve-2026-08-27.md @@ -7,7 +7,7 @@ > [`docs/audit/affirmations.md`](affirmations.md). - **Instance** : `instance` — inventaire `instance/inventories/production/hosts.yml` -- **Verdict** : ✅ CONFORME (50 OK · 0 echec · 0 saute) +- **Verdict** : ✅ CONFORME (51 OK · 0 echec · 0 saute) ## Preuves @@ -63,6 +63,7 @@ | P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 84 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | | P49 | Registre des flux : la matrice d'audit est a jour | — | ✅ OK | Le registre des flux reproduit exactement ce que les `meta/flux.yml` declarent (110 lignes). | | P50 | Silences : un refus muet est declare, place en dernier, et motive | — | ✅ OK | 2 silence(s) declare(s), tous en sequence > 1 (la plus haute des 121 regles `pass`), tous non consignes et tous motives. | +| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) appelee(s) par le moteur, toutes declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 | ## Couverture des affirmations ✅ du registre diff --git a/requirements.yml b/requirements.yml index c47b682..742f235 100644 --- a/requirements.yml +++ b/requirements.yml @@ -7,12 +7,36 @@ # poste du mainteneur. Une collection utilisee par un role et absente d'ici marche chez # le mainteneur et echoue partout ailleurs : c'est le genre d'ecart qu'on ne voit qu'en # portant le moteur sur une autre machine. +# LES VERSIONS SONT EPINGLEES (2026-08-27). Ce fichier nommait ses collections sans dire +# LESQUELLES : chaque machine installait donc ce qui etait courant le jour de son montage. +# +# Ce que ca a coute. Le runner du SITE, monte le 2026-08-26, a recu `community.general` +# 13.3.0 quand le poste du mainteneur porte la 10.3.0. Or la version 11 a RETIRE les +# modules Proxmox de cette collection — ils vivent desormais dans `community.proxmox`. Le +# runner a donc echoue a la premiere materialisation de VM, sur un +# « couldn't resolve module/action 'community.general.proxmox_pool' », alors que le meme +# depot, le meme playbook et le meme plan fonctionnaient chez le mainteneur. +# +# Un moteur qui ne dit pas de QUELLES versions il depend n'est pas portable : il est +# seulement chanceux. Nommer une collection sans l'epingler, c'est declarer une dependance +# sur « l'etat d'Internet a la date du deploiement ». +# +# MIGRATION CONNUE, PAS ENCORE FAITE : passer les quatre modules Proxmox +# (`proxmox_kvm`, `proxmox_pool`, `proxmox_nic`, `proxmox_disk`) a `community.proxmox` +# permettra de suivre `community.general` au-dela de la 11. Tant que ce n'est pas fait, +# l'epingle ci-dessous est ce qui tient. Les modules LDAP, eux, restent dans +# `community.general` — la migration ne les concerne pas. collections: - name: community.postgresql + version: 3.10.2 + # `<11.0.0` N'EST PAS UN CAPRICE : la 11 a sorti les modules Proxmox de cette collection. + # Les remonter demanderait de reecrire `playbooks/proxmox/*` — voir la note ci-dessus. - name: community.general + version: 10.3.0 # `serveur_backup` : ansible.posix.authorized_key. Utilisee depuis longtemps, jamais # declaree — donc absente du runner, qui n'aurait pas pu deployer les sauvegardes. - name: ansible.posix + version: 1.6.2 # `community.docker` a ete RETIREE le 2026-08-24 : `serveur_collabora` etait la seule # exception conteneurisee de la flotte, et il tourne desormais en natif (paquet coolwsd # du depot amont Collabora). Plus aucun role n'a besoin de Docker. diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index fd4b22c..8dffd42 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -237,9 +237,18 @@ ansible.builtin.command: cmd: >- {{ serveur_ops_venv }}/bin/ansible-galaxy collection install - -r requirements.yml + -r requirements.yml --force -p {{ serveur_ops_racine }}/.ansible/collections chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne" + # `--force` PARCE QU'UN POSTE PEUT ETRE EN AVANCE, PAS SEULEMENT EN RETARD (2026-08-27). + # Sans lui, `ansible-galaxy` laisse en place une collection deja installee dans une + # version SUPERIEURE a celle demandee : il ne retrograde pas. Le runner du SITE, monte + # avant que `requirements.yml` n'epingle ses versions, portait `community.general` 13.3.0 + # — une version d'ou les modules Proxmox ont ete RETIRES. La premiere materialisation de + # VM depuis le runner a echoue la-dessus. Le depot doit faire autorite dans les DEUX + # sens : ce que `requirements.yml` declare est ce qui est installe, ni plus haut ni plus + # bas. Voir la preuve P51. + # # LE GARDE-FOU SUIVAIT LA MAUVAISE CHOSE (2026-08-24). Il etait `creates: …/community/general` : # une collection AJOUTEE a requirements.yml n'aurait jamais ete installee, puisque le # repertoire temoin existait deja. Le runner serait reste en retard sur le moteur, en diff --git a/scripts/prouver.py b/scripts/prouver.py index 20d9719..896bab9 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -1396,6 +1396,82 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]: f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.") +def preuve_collections_declarees_et_epinglees() -> tuple[bool, str]: + """Toute collection appelee par le moteur est DECLAREE, et a une version EPINGLEE. + + DEUX PANNES, LA MEME CAUSE — le moteur ne disait pas de quoi il depend. + + Le 2026-08-24, `ansible.posix` etait utilisee depuis longtemps sans figurer dans + `requirements.yml` : elle trainait sur le poste du mainteneur, et manquait partout + ailleurs. Le runner n'aurait pas pu deployer les sauvegardes. + + Le 2026-08-27, la meme faiblesse a frappe d'un autre cote. Les collections etaient + nommees mais SANS VERSION : le runner du site, monte un jour plus tard, a recu + `community.general` 13.3.0 quand le poste porte la 10.3.0. La version 11 ayant sorti + les modules Proxmox de cette collection, la premiere materialisation de VM depuis le + runner a echoue sur `couldn't resolve module/action community.general.proxmox_pool` + — avec le meme depot, le meme playbook et le meme plan que chez le mainteneur. + + Une dependance non epinglee n'est pas une dependance : c'est un pari sur l'etat + d'Internet a la date du deploiement. Un moteur qui se veut portable, et exploitable + par un sysadmin sans IA, doit pouvoir se reconstruire a l'identique dans six mois. + + On lit les MODULES REELLEMENT APPELES dans les fichiers YAML — la clef d'une tache, + pas une chaine dans du Python : `ansible.parsing.vault` est un import, pas une + collection, et l'y confondre ferait crier la preuve pour rien. + """ + req = RACINE / "requirements.yml" + if not req.is_file(): + return False, "requirements.yml est introuvable — le moteur ne declare aucune dependance." + try: + import yaml as _y + decl = (_y.safe_load(req.read_text(encoding="utf-8")) or {}).get("collections") or [] + except Exception as e: # noqa: BLE001 + return False, f"requirements.yml est illisible : {e}" + + declarees = {} + for c in decl: + if isinstance(c, dict) and c.get("name"): + declarees[str(c["name"])] = str(c.get("version") or "").strip() + elif isinstance(c, str): + declarees[c] = "" + + # Une clef de tache YAML : ` - community.general.ldap_entry:` ou ` module.x.y:` + motif = re.compile(r"^\s*(?:-\s+)?([a-z0-9_]+\.[a-z0-9_]+)\.[a-z0-9_]+\s*:", re.M) + # ON NE LIT QUE LES FICHIERS DE TACHES. Un `defaults/main.yml` peut contenir + # `net.ipv4.ip_forward: 1` — une clef de sysctl que le motif prendrait pour un module, + # et la preuve crierait a une collection manquante nommee `net.ipv4`. Un module ne + # peut apparaitre que la ou il y a des taches. + fichiers = list((RACINE / "playbooks").rglob("*.yml")) + for sous in ("tasks", "handlers"): + fichiers += list((RACINE / "roles").rglob(f"{sous}/*.yml")) + utilisees: dict[str, str] = {} + for f in fichiers: + try: + txt = f.read_text(encoding="utf-8") + except Exception: # noqa: BLE001 + continue + for coll in motif.findall(txt): + if coll == "ansible.builtin": + continue + utilisees.setdefault(coll, str(f.relative_to(RACINE))) + + manquantes = [f"{c} (ex. {utilisees[c]})" for c in sorted(utilisees) if c not in declarees] + sans_epingle = [c for c in sorted(declarees) if not declarees[c]] + fautes = [] + if manquantes: + fautes.append("appelee(s) mais NON DECLAREE(S) dans requirements.yml : " + + ", ".join(manquantes)) + if sans_epingle: + fautes.append("declaree(s) SANS VERSION — le deploiement prendra ce qui passe : " + + ", ".join(sans_epingle)) + if fautes: + return False, " | ".join(fautes) + return True, (f"{len(utilisees)} collection(s) appelee(s) par le moteur, toutes " + f"declarees et epinglees : " + + ", ".join(f"{c}=={declarees[c]}" for c in sorted(utilisees))) + + def preuve_silences_declares() -> tuple[bool, str]: """Un `block` muet doit etre DECLARE, place APRES les `pass`, et porter son motif. @@ -1854,6 +1930,8 @@ PREUVES: list[dict] = [ "refs": [], "func": preuve_registre_flux_a_jour}, {"id": "P50", "titre": "Silences : un refus muet est declare, place en dernier, et motive", "refs": [], "func": preuve_silences_declares}, + {"id": "P51", "titre": "Collections : toutes declarees, toutes epinglees", + "refs": [], "func": preuve_collections_declarees_et_epinglees}, {"id": "P43", "titre": "Frontiere : le devis voit les machines du site", "refs": [], "func": preuve_devis_frontiere_du_site}, {"id": "P33", "titre": "Aucune collision de port entre roles co-localises", "refs": [],