diff --git a/docs/audit/preuve-2026-08-27.md b/docs/audit/preuve-2026-08-27.md index 8230065..f0d0fa0 100644 --- a/docs/audit/preuve-2026-08-27.md +++ b/docs/audit/preuve-2026-08-27.md @@ -63,7 +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 | +| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python 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-python.txt b/requirements-python.txt new file mode 100644 index 0000000..2c688ec --- /dev/null +++ b/requirements-python.txt @@ -0,0 +1,27 @@ +# Bibliotheques Python requises par le CONTROLEUR Set-OPS. +# +# LE PENDANT DE `requirements.yml`, POUR PYTHON (2026-08-27). Les collections Ansible +# etaient declarees ; les bibliotheques dont leurs modules dependent ne l'etaient nulle +# part. Le venv d'un poste d'exploitation recevait `ansible-core` et `pyyaml`, ecrits en +# dur dans le role — et rien d'autre. +# +# Ce que ca a coute : le runner du SITE a echoue a la premiere materialisation de VM sur +# « La bibliotheque Python proxmoxer est absente ». Le poste du mainteneur l'avait par +# son paquet systeme, sans que personne ne l'ait jamais declare. +# +# CE FICHIER NE LISTE QUE CE QUI TOURNE SUR LE CONTROLEUR. Les modules `ldap_*` et +# `postgresql_*` s'executent SUR LEURS CIBLES : `python-ldap` et `psycopg2` sont donc +# affaire des roles `serveur_openldap` et `serveur_postgresql`, pas d'ici. Le poste du +# mainteneur ne les a pas non plus, et la flotte se deploie tres bien — c'est la preuve +# que la frontiere est au bon endroit. +# +# Les versions sont EPINGLEES, pour la meme raison que dans `requirements.yml` : une +# dependance non epinglee n'est pas une dependance, c'est un pari sur l'etat d'Internet +# a la date du deploiement. Voir la preuve P51. + +# `community.general.proxmox_kvm`, `proxmox_pool`, `proxmox_nic`, `proxmox_disk` — +# tous `delegate_to: localhost`, donc tous exiges sur le CONTROLEUR. +proxmoxer==2.2.0 +# Transport HTTP de proxmoxer. `pip` l'installerait en dependance, mais une dependance +# transitive non nommee est une version non choisie. +requests==2.32.3 diff --git a/roles/serveur_ops/tasks/main.yml b/roles/serveur_ops/tasks/main.yml index 8dffd42..cdbdba9 100644 --- a/roles/serveur_ops/tasks/main.yml +++ b/roles/serveur_ops/tasks/main.yml @@ -78,6 +78,21 @@ become: true become_user: "{{ serveur_ops_utilisateur }}" +# LES BIBLIOTHEQUES DU CONTROLEUR SE DECLARENT, ELLES AUSSI (2026-08-27). Cette liste +# etait ecrite en dur — `ansible-core` et `pyyaml`, rien de plus — alors que les modules +# Proxmox du moteur exigent `proxmoxer` sur la machine qui les execute. Le runner du SITE +# a donc echoue a sa premiere materialisation de VM, sur une bibliotheque que le poste du +# mainteneur possedait par son paquet systeme sans que personne ne l'ait declaree. +# `requirements-python.txt` est desormais la source, au meme titre que `requirements.yml` +# pour les collections. Voir la preuve P51. +- name: Deposer la liste des bibliotheques Python du controleur + ansible.builtin.copy: + src: "{{ playbook_dir }}/../../requirements-python.txt" + dest: "{{ serveur_ops_racine }}/requirements-python.txt" + owner: "{{ serveur_ops_utilisateur }}" + group: "{{ serveur_ops_utilisateur }}" + mode: "0644" + - name: Installer Ansible dans l'environnement isolé ansible.builtin.pip: name: @@ -93,6 +108,22 @@ delay: 6 until: serveur_ops_pip is succeeded +# TACHE SEPAREE : dans `ansible.builtin.pip`, `name` et `requirements` sont MUTUELLEMENT +# EXCLUSIFS. Les fusionner ne produit pas une erreur parlante — le module ignore l'un des +# deux, et la bibliotheque manquante ne se revele qu'au premier module qui en depend. +- name: Installer les bibliotheques Python du controleur (source declaree) + ansible.builtin.pip: + requirements: "{{ serveur_ops_racine }}/requirements-python.txt" + virtualenv: "{{ serveur_ops_venv }}" + state: present + become: true + become_user: "{{ serveur_ops_utilisateur }}" + when: not ansible_check_mode + register: serveur_ops_pip_controleur + retries: 3 + delay: 6 + until: serveur_ops_pip_controleur is succeeded + # L'OUTILLAGE DOIT ETRE SUR LE CHEMIN DE CELUI QUI L'EXPLOITE (2026-08-26). # # Ansible vit dans un venv isole — c'est voulu, il est epingle. Mais rien ne le mettait sur @@ -233,13 +264,41 @@ # archives en RELATIF (`community-postgresql-4.2.0.tar.gz`), et ansible-galaxy les cherche # depuis le repertoire COURANT, pas depuis celui du fichier. Sans chdir : « Could not find # community-postgresql-4.2.0.tar.gz » alors que l'archive est bien la, a cote. +# LE GARDE-FOU DOIT SUIVRE LE RESULTAT, PAS LE GESTE (2026-08-27). La condition +# d'installation ne regardait que le DEPOT des archives. Le jour ou l'on a corrige le +# CHEMIN d'installation, les archives etaient inchangees : le role a donc conclu qu'il +# n'y avait rien a faire, et les collections sont restees a l'ancien endroit. Le meme +# piege qu'en 2026-08-24, deplace d'un cran. On mesure desormais la DESTINATION. +- name: Voir si les collections sont deja la ou le moteur les cherche + ansible.builtin.stat: + path: >- + {{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') + | first).dest }}/.ansible/collections/ansible_collections/community/general/MANIFEST.json + register: serveur_ops_collections_en_place + become: true + become_user: "{{ serveur_ops_utilisateur }}" + - name: Installer les collections depuis le depot local (hors ligne) ansible.builtin.command: cmd: >- {{ serveur_ops_venv }}/bin/ansible-galaxy collection install -r requirements.yml --force - -p {{ serveur_ops_racine }}/.ansible/collections + -p {{ serveur_ops_racine }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur') + | first).dest }}/.ansible/collections chdir: "{{ serveur_ops_racine }}/.collections-hors-ligne" + # INSTALLER LA OU LE MOTEUR REGARDE (2026-08-27). Le `Makefile` pose + # `export ANSIBLE_HOME ?= $(CURDIR)/.ansible` : sous `make`, Ansible ne cherche donc ses + # collections QUE dans `/.ansible/collections`. Ce role les deposait dans + # `/.ansible/collections` — a cote, jamais lu. Le runner du SITE echouait ainsi + # sur `couldn't resolve module/action community.general.proxmox_pool` alors que la + # collection etait bel et bien installee, complete, et visible par `ansible-doc`. + # + # POURQUOI LE MAINTENEUR NE VOYAIT RIEN : sur son poste, les collections viennent du + # paquet systeme (`/usr/lib/python3/dist-packages/ansible_collections`), toujours dans + # le chemin de recherche quel que soit `ANSIBLE_HOME`. Le reglage du Makefile n'y a + # jamais eu de consequence. Sur un runner, ou rien n'est installe par la distribution, + # il decide de tout. Encore un ecart qu'on ne voit qu'en portant le moteur ailleurs. + # # `--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 @@ -263,6 +322,7 @@ # d'autre ne l'aurait vu. Les conditions vont donc dans UNE liste. when: - serveur_ops_depot_collections is changed + or not serveur_ops_collections_en_place.stat.exists - not ansible_check_mode # LES DEUX SYMLINKS (D-80). `instance` dit QUEL tenant on pilote. Le poste en pose un par diff --git a/scripts/prouver.py b/scripts/prouver.py index 896bab9..1b01b32 100644 --- a/scripts/prouver.py +++ b/scripts/prouver.py @@ -1465,9 +1465,35 @@ def preuve_collections_declarees_et_epinglees() -> tuple[bool, str]: if sans_epingle: fautes.append("declaree(s) SANS VERSION — le deploiement prendra ce qui passe : " + ", ".join(sans_epingle)) + # LES BIBLIOTHEQUES PYTHON DU CONTROLEUR, MEME EXIGENCE (2026-08-27). Les collections + # etaient declarees ; ce dont leurs modules dependent ne l'etait nulle part. Le venv + # d'un poste recevait `ansible-core` et `pyyaml`, ecrits en dur dans le role. Le runner + # a echoue sur « la bibliotheque Python proxmoxer est absente » — que le poste du + # mainteneur possedait par son paquet systeme, sans que personne ne l'ait declaree. + py = RACINE / "requirements-python.txt" + if not py.is_file(): + fautes.append("requirements-python.txt est absent : les bibliotheques du " + "controleur ne sont declarees nulle part.") + else: + lignes = [l.strip() for l in py.read_text(encoding="utf-8").splitlines() + if l.strip() and not l.strip().startswith("#")] + floues = [l for l in lignes if "==" not in l] + if not lignes: + fautes.append("requirements-python.txt ne declare aucune bibliotheque.") + if floues: + fautes.append("bibliotheque(s) Python sans version exacte : " + ", ".join(floues)) + # Le moteur appelle des modules Proxmox EN LOCAL : `proxmoxer` est donc exige sur + # le controleur. C'est la dependance dont l'absence a bloque la premiere + # materialisation depuis le runner ; on la nomme pour qu'elle ne reparte pas. + if any((RACINE / "playbooks").rglob("*proxmox*")) and \ + not any(l.lower().startswith("proxmoxer") for l in lignes): + fautes.append("le moteur porte des playbooks Proxmox mais ne declare pas " + "`proxmoxer`, exige sur le CONTROLEUR.") + py_n = len(lignes) + if fautes: return False, " | ".join(fautes) - return True, (f"{len(utilisees)} collection(s) appelee(s) par le moteur, toutes " + return True, (f"{len(utilisees)} collection(s) et {py_n} bibliotheque(s) Python " f"declarees et epinglees : " + ", ".join(f"{c}=={declarees[c]}" for c in sorted(utilisees)))