Compare commits
No commits in common. "35853ff5fb73541f052974490339945af0eb1d59" and "561c034eb447f6d86aab6a33f8f2e7c65cf214e3" have entirely different histories.
35853ff5fb
...
561c034eb4
12 changed files with 14 additions and 863 deletions
124
CHANGELOG.md
124
CHANGELOG.md
|
|
@ -1,129 +1,5 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-27 — P52 : matérialiser n'exige pas d'entrer chez le tenant
|
||||
|
||||
**52 preuves.** `creer-vm` confirmait son succès en attendant une réponse SSH. Le runner du
|
||||
SITE matérialise le terrain de **tous** les tenants, mais la frontière lui refuse d'entrer
|
||||
chez eux — c'est le sens même de leur isolation. La première VM qu'il a créée a donc été
|
||||
déclarée en échec après 600 secondes alors qu'elle tournait, avec l'adresse exacte que le
|
||||
plan lui destinait :
|
||||
|
||||
```
|
||||
Attente de SSH sur ops-01 ............ ECHEC: injoignable apres 600s.
|
||||
```
|
||||
|
||||
**La tentation était d'ouvrir le SSH du runner vers tous les tenants.** Ça aurait réparé la
|
||||
mesure en détruisant ce qu'elle protège : une machine capable d'entrer chez chaque locataire
|
||||
est précisément ce que cette architecture refuse d'avoir.
|
||||
|
||||
L'agent invité répond sans rien ouvrir — l'API des hyperviseurs est déjà le flux par lequel
|
||||
la VM vient d'être créée, donc qui peut la créer peut la voir naître — et il **prouve
|
||||
davantage**. « Quelque chose écoute sur le port 22 » ne dit ni quel système a démarré, ni si
|
||||
cloud-init a posé la bonne adresse. Sur `ops-01` :
|
||||
|
||||
```
|
||||
10.17.19.41 portee sur asgard — Debian GNU/Linux 13 (trixie) 6.12.101
|
||||
```
|
||||
|
||||
Ses échecs distinguent deux causes très différentes : « la machine démarre mais rapporte une
|
||||
**autre** adresse — cloud-init, ou le pont sur lequel elle est posée », et « introuvable sur
|
||||
la fabric ».
|
||||
|
||||
**Attendre la disponibilité a changé de main.** Guetter cloud-init et la libération de dpkg
|
||||
appartient à qui va **configurer** : `deployer` le fait désormais, là où il se contentait
|
||||
d'un `ping` unique. Il y gagne ce qu'il n'avait pas — attendre le verrou APT, faute de quoi
|
||||
la première couche échouait dessus. Contrepartie assumée : un nom d'hôte erroné patiente au
|
||||
lieu d'échouer vite ; échouer vite interdirait de chaîner création et déploiement, le geste
|
||||
central d'une reconstruction.
|
||||
|
||||
*Une preuve qui exige un pouvoir que l'architecture refuse ne mesure pas le système : elle
|
||||
mesure ce qu'il faudrait casser pour la satisfaire.*
|
||||
|
||||
## 2026-08-27 — P51 : quatre défauts de portabilité, révélés par le runner
|
||||
|
||||
**51 preuves.** Première matérialisation d'une VM de tenant depuis le runner du SITE. Elle a
|
||||
échoué quatre fois d'affilée, sur quatre dépendances que le moteur ne déclarait nulle part.
|
||||
Chacune fonctionnait chez le mainteneur pour une raison **différente**, et aucune de ces
|
||||
raisons n'existe sur une autre machine.
|
||||
|
||||
**1. Versions des collections.** `requirements.yml` les nommait sans les épingler. Le runner
|
||||
a reçu `community.general` 13.3.0 quand le poste porte la 10.3.0 — et la 11 a retiré les
|
||||
modules Proxmox de cette collection (ils vivent désormais dans `community.proxmox`).
|
||||
|
||||
```
|
||||
ERROR! couldn't resolve module/action 'community.general.proxmox_pool'
|
||||
```
|
||||
|
||||
*Une dépendance non épinglée n'est pas une dépendance, c'est un pari sur l'état d'Internet à
|
||||
la date du déploiement.*
|
||||
|
||||
**2. Installation en avant.** `ansible-galaxy` ne rétrograde pas : déclarer la 10.3.0 ne
|
||||
suffisait pas à défaire une 13.3.0 déjà posée. `--force`. Un poste peut être en **avance**,
|
||||
pas seulement en retard, et le dépôt doit faire autorité dans les deux sens.
|
||||
|
||||
**3. Emplacement.** Le `Makefile` pose `ANSIBLE_HOME ?= $(CURDIR)/.ansible` : sous `make`,
|
||||
Ansible ne lit **que** `<moteur>/.ansible/collections`. Le rôle déposait dans
|
||||
`<racine>/.ansible/collections` — à côté, jamais lu. Invisible chez le mainteneur, où les
|
||||
collections viennent du paquet système, toujours dans le chemin quel que soit `ANSIBLE_HOME`.
|
||||
Et le garde-fou d'installation suivait le **dépôt des archives** : changer la destination ne
|
||||
le déclenchait pas. Il mesure désormais la destination — même piège qu'au 2026-08-24, déplacé
|
||||
d'un cran.
|
||||
|
||||
**4. Bibliothèques Python.** Le venv recevait `ansible-core` et `pyyaml`, écrits en dur. Les
|
||||
modules Proxmox tournent `delegate_to: localhost` et exigent `proxmoxer` **sur le
|
||||
contrôleur**.
|
||||
|
||||
```
|
||||
La bibliotheque Python proxmoxer est absente
|
||||
```
|
||||
|
||||
Né d'ici : `requirements-python.txt`, avec sa frontière écrite — il ne déclare **que** ce qui
|
||||
tourne sur le contrôleur. `python-ldap` et `psycopg2` s'exécutent sur leurs cibles ; le poste
|
||||
du mainteneur ne les a pas et la flotte se déploie, ce qui prouve que la ligne est au bon
|
||||
endroit.
|
||||
|
||||
**P51** garde les trois faiblesses de cette famille : une dépendance utilisée sans être
|
||||
déclarée (le défaut du 2026-08-24, `ansible.posix`), une dépendance déclarée sans version, et
|
||||
`proxmoxer` absent alors que des playbooks Proxmox existent. La preuve ne lit que les fichiers
|
||||
de **tâches** : un `defaults/main.yml` porte `net.ipv4.ip_forward`, qu'un motif trop large
|
||||
prendrait pour un module.
|
||||
|
||||
**Résultat :** `ops-01` matérialisée par le runner du site. L'agent invité rapporte
|
||||
`eth0 10.17.19.41/24`, Debian 13 trixie — l'adressage dérivé du plan, appliqué.
|
||||
|
||||
**Migration connue, pas faite :** passer les quatre modules Proxmox à `community.proxmox`
|
||||
permettra de suivre `community.general` au-delà de la 11.
|
||||
|
||||
*Un seul exécutant masque ces écarts indéfiniment ; un second les révèle tous en une soirée.
|
||||
Même mécanique que le second tenant en août.*
|
||||
|
||||
## 2026-08-27 — P50 : le devis de la frontière sait refuser SANS consigner
|
||||
|
||||
**50 preuves.** L'outil ne savait qu'**autoriser** — `"action": "pass"` était en dur dans
|
||||
l'émetteur. Une règle de silence ne pouvait donc pas naître du dépôt, et j'en avais posé deux
|
||||
à la main sur le boîtier : exactement ce que ce projet refuse.
|
||||
|
||||
**Ce qui l'a motivé.** Le journal de la frontière écrivait 982 000 entrées par jour, dont
|
||||
82 % un balayage Internet contre le port VNC et le reste du bavardage de découverte du réseau
|
||||
local. Sa fenêtre utile était tombée à **quarante-quatre secondes**. J'y ai cherché la trace
|
||||
d'un flux du site vers les hyperviseurs, je n'ai rien trouvé, et j'en ai conclu à tort
|
||||
qu'aucune règle ne bloquait. **Un journal noyé ment aussi sûrement qu'un journal mort.**
|
||||
Mesuré après déclaration : ~20 700/jour.
|
||||
|
||||
Une règle de silence ne change **aucun** comportement : ce qu'elle vise était déjà refusé par
|
||||
le défaut. Elle ne supprime qu'une trace que personne ne lira.
|
||||
|
||||
**Trois pièces.** `cle_regle` accepte une action sans changer d'un octet la clé des règles
|
||||
`pass` déjà posées — l'ajout naïf d'un champ les aurait toutes détruites pour les recréer à
|
||||
l'identique, sur la frontière, en production ; le plan l'a confirmé : 7 à créer, 0 à retirer,
|
||||
121 inchangées. `_corps_regle` lit l'action, la consignation et la **séquence** depuis le
|
||||
devis : la séquence est ce qui rend un `block` sûr, puisque OPNsense évalue en `quick` et
|
||||
qu'un blocage large émis avant les `pass` fermerait courrier, web et accès distant.
|
||||
`devis_opnsense` lit `opnsense_silences` et en fabrique règles et alias, **motif compris** —
|
||||
une règle `block` muette sans raison écrite est indiscernable d'un oubli.
|
||||
|
||||
P50 garde ces deux dangers : un silence en séquence 1 échoue, un silence sans motif échoue.
|
||||
|
||||
## 2026-08-26 — P49 : le registre des flux avait dérivé sans bruit
|
||||
|
||||
**48 preuves.** `docs/registre-flux.md` est **généré** depuis les `roles/*/meta/flux.yml`,
|
||||
|
|
|
|||
35
Makefile
35
Makefile
|
|
@ -355,17 +355,6 @@ appliquer: ansible-runtime ## Applique un groupe a la flotte — GROUPE=<groupe>
|
|||
python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) --dependances $(FICHIER_DEPENDANCES) verifier-dependances-groupe --groupe $(GROUPE)
|
||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$(DOSSIER_PLAYBOOKS_GROUPES)/$(GROUPE).yml" --limit '$(GROUPE):&$(GROUPE_HOTES_ACTIFS)'
|
||||
|
||||
# ATTENDRE PLUTOT QUE CONSTATER (2026-08-27). `deployer` faisait un `ping` unique
|
||||
# (`_verifier-acces-hote`). Depuis que `creer-vm` ne guette plus le SSH — il prouve la
|
||||
# materialisation par l'agent invite, seul geste que le runner du SITE ait le droit de
|
||||
# faire chez un tenant — c'est ICI qu'il faut attendre : enchainer `creer-vm` puis
|
||||
# `deployer` tomberait sinon sur une machine encore en cloud-init. On y gagne au passage
|
||||
# ce que le `ping` unique ne faisait pas : attendre que `dpkg` se libere, faute de quoi
|
||||
# la premiere couche echoue sur un verrou APT.
|
||||
#
|
||||
# CONTREPARTIE ASSUMEE : un nom d'hote errone ne fait plus echouer tout de suite, il
|
||||
# patiente ATTENTE_HOTE secondes. Echouer vite sur l'injoignable interdirait de chainer
|
||||
# la creation au deploiement — le geste central d'une reconstruction.
|
||||
deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre du graphe — HOTE=<nom>
|
||||
@set -e; \
|
||||
if [[ -z "$(HOTE)" ]]; then \
|
||||
|
|
@ -380,7 +369,7 @@ deployer: _instance-requise ## Deploie un hote, couche par couche, dans l'ordre
|
|||
exit 2; \
|
||||
fi; \
|
||||
$(VAULT_UNE_FOIS) \
|
||||
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
|
||||
$(MAKE) _verifier-acces-hote LIMITE="$(HOTE)"; \
|
||||
$(MAKE) _verifier-privileges-hote LIMITE="$(HOTE)"; \
|
||||
for playbook in $$playbooks; do \
|
||||
ansible-playbook -i $(INVENTAIRE_PRODUCTION) "$$playbook" --limit "$(HOTE)"; \
|
||||
|
|
@ -1029,25 +1018,11 @@ creer-vm: _instance-requise ## Cree une VM et attend qu'elle soit joignable —
|
|||
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
|
||||
DEMARRER="$(DEMARRER)" \
|
||||
CLONE_COMPLET="$(CLONE_COMPLET)"
|
||||
@# `creer-vm` CONFIRME LA MATERIALISATION, PAS LA JOIGNABILITE (2026-08-27). Il
|
||||
@# attendait une reponse SSH — un geste que le runner du SITE ne peut pas faire : la
|
||||
@# frontiere lui refuse d'entrer chez les tenants, et c'est le sens meme de leur
|
||||
@# isolation. La premiere VM creee par le runner a donc ete declaree en echec apres
|
||||
@# 600 secondes alors qu'elle tournait, avec la bonne adresse.
|
||||
@#
|
||||
@# L'agent invite repond a cette question sans rien ouvrir — l'API des hyperviseurs
|
||||
@# est deja le flux par lequel la VM vient d'etre creee — et il PROUVE DAVANTAGE :
|
||||
@# il rapporte le systeme, le noyau et l'adresse effectivement portee, la ou SSH ne
|
||||
@# disait que « quelque chose ecoute sur le port 22 ».
|
||||
@#
|
||||
@# ATTENDRE LA DISPONIBILITE est un autre geste, deplace chez `deployer` : cloud-init
|
||||
@# et dpkg se guettent juste avant de configurer, par celui qui va configurer.
|
||||
@# `creer-vm` rend une VM PRETE, pas seulement demarree : sans cette attente,
|
||||
@# enchainer `creer-vm` puis `deployer` echoue presque toujours sur une machine
|
||||
@# neuve. C'est ce qui separe une suite de commandes d'une reconstruction.
|
||||
@if [[ "$(ATTENDRE)" != "false" ]]; then \
|
||||
params="$$(python3 scripts/inventory_host.py --inventaire $(INVENTAIRE_PRODUCTION) parametres-proxmox --hote $(HOTE))"; \
|
||||
eval "$$params"; \
|
||||
python3 scripts/attendre_materialisation.py \
|
||||
--vmid "$$SETOPS_VMID" --ip "$$SETOPS_IP" --hote "$(HOTE)" \
|
||||
--delai $(ATTENTE_HOTE); \
|
||||
$(MAKE) --no-print-directory _attendre-hote LIMITE="$(HOTE)"; \
|
||||
fi
|
||||
|
||||
inventaire-verifier: ansible-runtime _instance-requise ## Verifie que l'inventaire se parse (voute dechiffree)
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-08-27
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/production/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (52 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). |
|
||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 37 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 35 rôles, 92 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_oauth2_proxy |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| 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 | 5 hotes, 14 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 : 18 secret(s) exige(s), tous presents. Voute reelle : 21 cle(s), aucun manque. |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 14 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. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 11 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 79 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 5 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| 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. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 14 exigence(s) de role, toutes satisfaites (39 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 (16 groupes). |
|
||||
| P34 | Chaque document declare son lecteur | — | ✅ OK | 42 document(s) declarent leur lecteur (25 genere(s) exempte(s)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 0 application(s) exigeant une base l'ont toutes (0 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 2 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| 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). |
|
||||
| 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, 56 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| 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) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-08-27._
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-08-28
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (52 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | [0m[0m |
|
||||
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 4 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). |
|
||||
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 37 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 35 rôles, 92 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 11 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 3 tenant(s), 50 groupe(s), 79 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 15 hote(s) x 5 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| 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. |
|
||||
| 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)). |
|
||||
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). |
|
||||
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| 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). |
|
||||
| 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, 56 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 4 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| 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) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-08-28._
|
||||
|
|
@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
|
|||
| rôles | 61 | `roles/*/` |
|
||||
| README de rôles | 61 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 38 | `docs/*.md` |
|
||||
| pièces d'audit | 29 | `docs/audit/*` |
|
||||
| pièces d'audit | 27 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 78 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
# 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
|
||||
|
|
@ -7,36 +7,12 @@
|
|||
# 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.
|
||||
|
|
|
|||
|
|
@ -78,21 +78,6 @@
|
|||
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:
|
||||
|
|
@ -108,22 +93,6 @@
|
|||
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
|
||||
|
|
@ -264,50 +233,13 @@
|
|||
# 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 }}/{{ (serveur_ops_depots | selectattr('role', 'eq', 'moteur')
|
||||
| first).dest }}/.ansible/collections
|
||||
-r requirements.yml
|
||||
-p {{ serveur_ops_racine }}/.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 `<moteur>/.ansible/collections`. Ce role les deposait dans
|
||||
# `<racine>/.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
|
||||
# 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
|
||||
|
|
@ -322,7 +254,6 @@
|
|||
# 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
|
||||
|
|
|
|||
|
|
@ -111,14 +111,7 @@ def cle_regle(r: dict) -> str:
|
|||
une regle a retirer, ce qui est exactement la verite.
|
||||
"""
|
||||
ports = ",".join(map(str, r["ports"] or ["*"]))
|
||||
# L'ACTION N'ENTRE DANS LA CLE QUE SI ELLE N'EST PAS `pass` (2026-08-27). Les regles
|
||||
# de SILENCE — des `block` non consignes — ont besoin d'une identite distincte, mais
|
||||
# l'ajout naif d'un champ aurait change la cle des 117 regles deja posees : le
|
||||
# rapprochement les aurait toutes detruites pour les recreer a l'identique, sur la
|
||||
# frontiere, en production. Une marque vide pour le cas majoritaire garde ces cles
|
||||
# octet pour octet, et n'en donne une nouvelle qu'a ce qui est nouveau.
|
||||
marque = "" if str(r.get("action") or "pass") == "pass" else f"{r['action']}:"
|
||||
return (f"setops:{marque}{r['tenant']}:{r['interface']}:{r['sens']}:{r['protocole']}:"
|
||||
return (f"setops:{r['tenant']}:{r['interface']}:{r['sens']}:{r['protocole']}:"
|
||||
f"{r['source']}->{r['destination']}:{ports}")
|
||||
|
||||
|
||||
|
|
@ -181,29 +174,14 @@ def _corps_regle(r: dict, k: str) -> dict:
|
|||
# le champ `destination_not` d'OPNsense.
|
||||
dst = str(r["destination"])
|
||||
nie = dst.startswith("!")
|
||||
# L'ACTION ET LA CONSIGNATION VIENNENT DU DEVIS (2026-08-27). Elles etaient en dur :
|
||||
# l'outil ne savait qu'AUTORISER, et une regle qui fait taire du bruit deja refuse
|
||||
# n'avait donc aucun moyen de naitre du depot. Les defauts reproduisent exactement
|
||||
# l'ancien comportement — `pass`, non consigne, sequence 1 — pour que les regles
|
||||
# existantes soient emises a l'identique.
|
||||
#
|
||||
# LA SEQUENCE EST CE QUI REND UN `block` SUR : OPNsense evalue en `quick`, donc la
|
||||
# PREMIERE regle qui correspond gagne. Un blocage large pose avant les `pass` du
|
||||
# devis fermerait le courrier, le web et l'acces distant. Les silences se declarent
|
||||
# a 900, apres tout le reste.
|
||||
corps = {
|
||||
"enabled": "1",
|
||||
"sequence": str(r.get("sequence") or "1"),
|
||||
"action": str(r.get("action") or "pass"),
|
||||
"log": "1" if r.get("journaliser") else "0",
|
||||
"sequence": "1",
|
||||
"action": "pass",
|
||||
"interface": r["interface"],
|
||||
"direction": "in", # le devis raisonne en ARRIVEE sur l'interface (D-61)
|
||||
"ipprotocol": "inet",
|
||||
# `any` reste EN MINUSCULES : OPNsense attend ses protocoles en majuscules mais
|
||||
# nomme « tout protocole » `any`. Un `ANY` majuscule est rejete a la validation.
|
||||
"protocol": ("any" if r["protocole"] == "any"
|
||||
else "ICMP" if r["protocole"] == "icmp"
|
||||
else r["protocole"].upper()),
|
||||
"protocol": "ICMP" if r["protocole"] == "icmp" else r["protocole"].upper(),
|
||||
"source_net": r["source"],
|
||||
"destination_net": dst[1:] if nie else dst,
|
||||
"destination_not": "1" if nie else "0",
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Attend qu'une VM soit MATERIALISEE : demarree, et portant l'adresse que le plan lui destine.
|
||||
|
||||
POURQUOI PAS SSH (mesure du 2026-08-27). `creer-vm` confirmait son succes en attendant
|
||||
une reponse SSH. Deux choses clochaient.
|
||||
|
||||
La premiere est une question de PERIMETRE. Le runner du SITE materialise le terrain de
|
||||
tous les tenants, mais la frontiere lui refuse — deliberement — d'entrer chez eux : c'est
|
||||
le sens meme de l'isolation inter-tenant. La premiere VM creee par le runner a donc ete
|
||||
declaree en echec apres 600 secondes alors qu'elle tournait parfaitement. Ouvrir le SSH
|
||||
du runner vers tous les tenants aurait « repare » la mesure en detruisant ce qu'elle
|
||||
mesurait : une machine capable d'entrer chez chaque locataire est exactement ce que
|
||||
l'architecture cherche a ne pas avoir.
|
||||
|
||||
La seconde est que SSH PROUVE MOINS. « Quelque chose ecoute sur le port 22 » ne dit ni
|
||||
quel systeme a demarre, ni si cloud-init a pose la bonne adresse. L'agent invite, lui,
|
||||
rapporte le systeme, le noyau et l'adresse EFFECTIVEMENT portee par l'interface — on
|
||||
verifie donc que la machine est celle que le plan decrivait, pas seulement qu'une machine
|
||||
repond.
|
||||
|
||||
Et il n'ouvre rien : l'API des hyperviseurs est deja le flux par lequel on a cree la VM.
|
||||
Qui peut la creer peut la voir naitre.
|
||||
|
||||
ATTENDRE LA DISPONIBILITE EST UN AUTRE GESTE, qui appartient a qui va CONFIGURER la
|
||||
machine : `make deployer` attend desormais SSH, cloud-init et la liberation de dpkg,
|
||||
juste avant d'en avoir besoin. Materialiser et configurer sont deux metiers.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from proxmox_api import Cluster # noqa: E402
|
||||
|
||||
|
||||
def _noeud_de(api: Cluster, vmid: str) -> str | None:
|
||||
"""Sur quel noeud vit ce VMID. Le plan ne le dit pas toujours — la fabric, si."""
|
||||
noeuds = api("/nodes")
|
||||
if not isinstance(noeuds, list):
|
||||
return None
|
||||
for n in noeuds:
|
||||
nom = n.get("node")
|
||||
vms = api(f"/nodes/{nom}/qemu")
|
||||
if isinstance(vms, list) and any(str(v.get("vmid")) == str(vmid) for v in vms):
|
||||
return nom
|
||||
return None
|
||||
|
||||
|
||||
def _adresses(api: Cluster, noeud: str, vmid: str) -> list[str]:
|
||||
"""Les IPv4 que l'agent invite declare, hors boucle locale.
|
||||
|
||||
Un `_erreur` n'est pas un echec : tant que la machine demarre, l'agent ne repond pas
|
||||
encore. C'est la difference entre « pas encore » et « pas du tout », et seule
|
||||
l'expiration du delai tranche.
|
||||
"""
|
||||
rep = api(f"/nodes/{noeud}/qemu/{vmid}/agent/network-get-interfaces")
|
||||
if not isinstance(rep, dict) or "result" not in rep:
|
||||
return []
|
||||
out = []
|
||||
for i in rep["result"]:
|
||||
if i.get("name") == "lo":
|
||||
continue
|
||||
for a in i.get("ip-addresses") or []:
|
||||
if a.get("ip-address-type") == "ipv4":
|
||||
out.append(str(a.get("ip-address")))
|
||||
return out
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--vmid", required=True)
|
||||
ap.add_argument("--ip", required=True, help="l'adresse que le plan destine a cette VM")
|
||||
ap.add_argument("--hote", default="", help="son nom, pour les messages")
|
||||
ap.add_argument("--delai", type=int, default=600)
|
||||
ap.add_argument("--noeud", default="")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
api, _ = Cluster.depuis_hebergeur()
|
||||
nom = args.hote or f"VMID {args.vmid}"
|
||||
|
||||
noeud = args.noeud or _noeud_de(api, args.vmid)
|
||||
if not noeud:
|
||||
print(f"ECHEC: VMID {args.vmid} introuvable sur la fabric.", file=sys.stderr)
|
||||
return 4
|
||||
|
||||
print(f"Attente de la materialisation de {nom} ({args.ip}) ", end="", flush=True)
|
||||
fin = time.time() + args.delai
|
||||
vues: list[str] = []
|
||||
while time.time() < fin:
|
||||
vues = _adresses(api, noeud, args.vmid)
|
||||
if args.ip in vues:
|
||||
print(" ok")
|
||||
osinfo = api(f"/nodes/{noeud}/qemu/{args.vmid}/agent/get-osinfo")
|
||||
o = osinfo.get("result", {}) if isinstance(osinfo, dict) else {}
|
||||
print(f" {nom} : {args.ip} portee sur {noeud} — "
|
||||
f"{o.get('pretty-name') or o.get('name') or 'systeme inconnu'} "
|
||||
f"{o.get('kernel-release', '')}".rstrip())
|
||||
return 0
|
||||
print(".", end="", flush=True)
|
||||
time.sleep(5)
|
||||
|
||||
print(f"\nECHEC: {nom} n'a pas porte {args.ip} apres {args.delai}s.", file=sys.stderr)
|
||||
if vues:
|
||||
print(f" L'agent invite rapporte pourtant : {', '.join(vues)}. "
|
||||
f"La machine demarre mais son adressage n'est pas celui du plan — "
|
||||
f"cloud-init, ou le pont/VLAN sur lequel elle est posee.", file=sys.stderr)
|
||||
else:
|
||||
print(" L'agent invite n'a rien rapporte : la machine ne demarre pas, ou "
|
||||
"`qemu-guest-agent` est absent du gabarit dore.", file=sys.stderr)
|
||||
return 4
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
|
|
@ -832,50 +832,6 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
|
|||
"tenant": "SITE",
|
||||
})
|
||||
|
||||
# LES SILENCES — refuser sans CONSIGNER (2026-08-27).
|
||||
#
|
||||
# Un pare-feu refuse deja tout ce qui n'est pas declare ; le probleme n'est pas ce
|
||||
# qu'il bloque, c'est ce qu'il ECRIT. Mesure du jour : 982 000 entrees par jour, dont
|
||||
# 82 % un balayage Internet contre le port VNC et le reste du bavardage de decouverte
|
||||
# (mDNS, SSDP, NetBIOS) du reseau local. La consequence n'est pas cosmetique — la
|
||||
# fenetre utile du journal tombait a QUARANTE-QUATRE SECONDES, moins que le temps de
|
||||
# reproduire un defaut. Un journal noye ne dit rien, exactement comme un journal mort.
|
||||
#
|
||||
# Une regle de silence ne change AUCUN comportement : ce qu'elle bloque etait deja
|
||||
# refuse par le defaut. Elle ne fait que taire une trace que personne ne lira. C'est
|
||||
# pourquoi elle se declare ici, avec son MOTIF : sans lui, une regle `block` muette
|
||||
# dans un pare-feu est indiscernable d'un oubli.
|
||||
for s in (intrants.get("opnsense_silences") or []):
|
||||
_dest = str(s.get("destination") or "any")
|
||||
# Une liste d'adresses devient un ALIAS, pas une regle par adresse : l'alias
|
||||
# nomme l'intention, et le boitier montre alors POURQUOI c'est tu.
|
||||
if s.get("destinations"):
|
||||
_nom_alias = f"SETOPS_SILENCE_{str(s['nom']).upper()}"
|
||||
alias[_nom_alias] = {
|
||||
"type": "network",
|
||||
"contenu": [str(x) for x in s["destinations"]],
|
||||
"description": f"Silence — {s.get('motif') or s['nom']}",
|
||||
}
|
||||
_dest = _nom_alias
|
||||
regles.append({
|
||||
"sens": "in",
|
||||
"interface": str(s.get("interface") or if_wan),
|
||||
"protocole": str(s.get("protocole") or "any"),
|
||||
"source": str(s.get("source") or "any"),
|
||||
"destination": _dest,
|
||||
"ports": [str(p) for p in (s.get("ports") or [])],
|
||||
"role": f"silence:{s.get('nom') or 'bruit'}",
|
||||
"tenant": "SILENCE",
|
||||
"action": "block",
|
||||
"journaliser": False,
|
||||
# 900 : APRES tous les `pass` (sequence 1) et Wireguard (101). OPNsense evalue
|
||||
# en `quick` — un blocage large place avant eux fermerait le courrier, le web
|
||||
# et l'acces distant d'un seul coup.
|
||||
"sequence": str(s.get("sequence") or 900),
|
||||
"chiffrement": "sans objet",
|
||||
"raison": str(s.get("motif") or "bruit de journal, deja refuse par defaut"),
|
||||
})
|
||||
|
||||
# Adresse du noeud de sortie primaire sur le lien de frontiere, calculee une fois :
|
||||
# elle sert au prochain saut des routes ET au commentaire qui l'explique.
|
||||
_ip_sortie = adresse_noeud_de_sortie(underlay_mod.charger(), transit)
|
||||
|
|
@ -1055,15 +1011,10 @@ def rendre(devis: dict) -> str:
|
|||
# Dans OPNsense une regle est TOUJOURS `in` sur l'interface d'arrivee : c'est
|
||||
# la que le paquet penetre le pare-feu. Le sens du flux (entrant/sortant vis-a-vis
|
||||
# de l'ecosysteme) se lit alors dans l'interface, pas dans un mot-cle.
|
||||
# L'ACTION S'AFFICHE (2026-08-27) : le rendu disait « pass » pour tout, ce qui
|
||||
# etait vrai tant que l'outil ne savait qu'autoriser. Un devis qui affiche
|
||||
# « pass » devant une regle de blocage serait pire qu'un devis muet.
|
||||
_act = str(rg.get("action") or "pass")
|
||||
_mut = "" if rg.get("journaliser", False) or _act == "pass" else " [non consigne]"
|
||||
out.append(
|
||||
f"{_act:4} in on {rg['interface']:8} proto {rg['protocole']:3} "
|
||||
f"pass in on {rg['interface']:8} proto {rg['protocole']:3} "
|
||||
f"from {rg['source']:24} to {rg['destination']:24} port {ports:7} "
|
||||
f"# {rg['role']} ({rg['chiffrement']}){_mut}"
|
||||
f"# {rg['role']} ({rg['chiffrement']})"
|
||||
)
|
||||
out.append(f"# {rg['raison']}")
|
||||
sortants = sum(1 for r in devis["regles"] if r["sens"] == "out")
|
||||
|
|
|
|||
|
|
@ -1396,214 +1396,6 @@ def preuve_carte_dit_vrai() -> tuple[bool, str]:
|
|||
f"{len(_carte_chiffres_mesures())} chiffres correspondent a la mesure.")
|
||||
|
||||
|
||||
def preuve_materialiser_n_exige_pas_d_entrer() -> tuple[bool, str]:
|
||||
"""Creer une VM ne demande pas d'entrer dans le tenant qui l'heberge.
|
||||
|
||||
POURQUOI (mesure du 2026-08-27). `creer-vm` confirmait son succes en attendant une
|
||||
reponse SSH. Le runner du SITE materialise le terrain de TOUS les tenants, mais la
|
||||
frontiere lui refuse d'entrer chez eux — c'est le sens meme de leur isolation. La
|
||||
premiere VM qu'il a creee a donc ete declaree en echec apres 600 secondes alors
|
||||
qu'elle tournait, avec l'adresse exacte que le plan lui destinait.
|
||||
|
||||
La tentation etait d'ouvrir le SSH du runner vers tous les tenants. Ca aurait
|
||||
« repare » la mesure en detruisant ce qu'elle protege : une machine capable d'entrer
|
||||
chez chaque locataire est precisement ce que cette architecture refuse d'avoir.
|
||||
|
||||
L'agent invite repond sans rien ouvrir, et prouve DAVANTAGE — le systeme, le noyau et
|
||||
l'adresse effectivement portee, la ou SSH ne disait que « quelque chose ecoute sur le
|
||||
port 22 ». Cette preuve garde le couplage ferme : materialiser et configurer sont deux
|
||||
metiers, et seul le second exige d'entrer.
|
||||
"""
|
||||
mk = RACINE / "Makefile"
|
||||
if not mk.is_file():
|
||||
return False, "Makefile introuvable."
|
||||
txt = mk.read_text(encoding="utf-8")
|
||||
debut = txt.find("\ncreer-vm:")
|
||||
if debut < 0:
|
||||
return False, "La cible `creer-vm` a disparu du Makefile."
|
||||
# La recette court jusqu'a la prochaine cible en debut de ligne.
|
||||
suite = txt[debut + 1:]
|
||||
fin = re.search(r"\n[A-Za-z_][A-Za-z0-9_.-]*:", suite)
|
||||
recette = suite[:fin.start()] if fin else suite
|
||||
|
||||
outil = RACINE / "scripts" / "attendre_materialisation.py"
|
||||
fautes = []
|
||||
if "_attendre-hote" in recette:
|
||||
fautes.append("`creer-vm` appelle `_attendre-hote`, qui exige SSH : un runner de "
|
||||
"SITE ne peut pas entrer chez un tenant, et ne doit pas pouvoir.")
|
||||
if not outil.is_file():
|
||||
fautes.append("scripts/attendre_materialisation.py est absent.")
|
||||
elif "attendre_materialisation.py" not in recette:
|
||||
fautes.append("`creer-vm` ne confirme plus la materialisation : ni SSH ni agent "
|
||||
"invite. Une creation qui ne verifie rien n'est pas une creation.")
|
||||
if fautes:
|
||||
return False, " | ".join(fautes)
|
||||
return True, ("`creer-vm` confirme par l'agent invite (API des hyperviseurs, deja "
|
||||
"utilisee pour creer), sans exiger d'entrer dans le tenant.")
|
||||
|
||||
|
||||
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))
|
||||
# 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) et {py_n} bibliotheque(s) Python "
|
||||
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.
|
||||
|
||||
POURQUOI (mesure du 2026-08-27). Le journal de la frontiere ecrivait 982 000 entrees
|
||||
par jour, dont 82 % un balayage Internet contre le port VNC. Sa fenetre utile etait
|
||||
tombee a QUARANTE-QUATRE SECONDES : j'y ai cherche la trace d'un flux du site vers les
|
||||
hyperviseurs, je n'ai rien trouve, et j'en ai conclu a tort qu'aucune regle ne
|
||||
bloquait. Un journal noye ment aussi surement qu'un journal mort.
|
||||
|
||||
La reponse — taire ce qui est deja refuse — introduit dans l'outil sa PREMIERE regle
|
||||
de blocage. Elle amene deux dangers que cette preuve garde :
|
||||
|
||||
1. L'ORDRE. OPNsense evalue en `quick` : la premiere regle qui correspond gagne. Un
|
||||
blocage large emis avant les `pass` du devis fermerait d'un coup le courrier, le
|
||||
web et l'acces distant. Les silences doivent donc porter une sequence STRICTEMENT
|
||||
superieure a celle de toute regle `pass`.
|
||||
|
||||
2. LE MOTIF. Une regle `block` sans raison ecrite est indiscernable d'un oubli pour
|
||||
qui relira cette frontiere dans six mois. Le silence se justifie ou n'existe pas.
|
||||
|
||||
On rejoue le devis en memoire — rien n'est ecrit, rien n'est joint sur le reseau.
|
||||
"""
|
||||
try:
|
||||
sys.path.insert(0, str(RACINE / "scripts"))
|
||||
import devis_opnsense as DO
|
||||
devis = DO.construire(DO.decouvrir_du_site())
|
||||
except Exception as e: # noqa: BLE001
|
||||
return False, (f"Le devis de la frontiere n'a pas pu etre rejoue : {e}. "
|
||||
f"Sans lui, on ne sait pas ce que la frontiere recevrait.")
|
||||
|
||||
regles = devis.get("regles") or []
|
||||
silences = [r for r in regles if str(r.get("action") or "pass") != "pass"]
|
||||
if not silences:
|
||||
return True, ("Aucune regle de blocage au devis — rien a garder. La preuve "
|
||||
"redeviendra parlante des qu'un silence sera declare.")
|
||||
|
||||
seq_pass = [int(r.get("sequence") or 1) for r in regles
|
||||
if str(r.get("action") or "pass") == "pass"]
|
||||
plancher = max(seq_pass) if seq_pass else 1
|
||||
fautes = []
|
||||
for r in silences:
|
||||
seq = int(r.get("sequence") or 1)
|
||||
if seq <= plancher:
|
||||
fautes.append(
|
||||
f"{r['interface']}/{r['role']} : sequence {seq} <= {plancher}, la plus "
|
||||
f"haute des regles `pass`. En `quick`, ce blocage gagnerait AVANT elles.")
|
||||
if r.get("journaliser"):
|
||||
fautes.append(f"{r['interface']}/{r['role']} : consigne — ce n'est pas un silence.")
|
||||
raison = str(r.get("raison") or "").strip()
|
||||
if len(raison) < 30:
|
||||
fautes.append(
|
||||
f"{r['interface']}/{r['role']} : motif absent ou trop court "
|
||||
f"({len(raison)} car.). Un blocage muet sans raison ecrite est un oubli.")
|
||||
if fautes:
|
||||
return False, " | ".join(fautes)
|
||||
return True, (f"{len(silences)} silence(s) declare(s), tous en sequence > {plancher} "
|
||||
f"(la plus haute des {len(seq_pass)} regles `pass`), tous non consignes "
|
||||
f"et tous motives.")
|
||||
|
||||
|
||||
def preuve_registre_flux_a_jour() -> tuple[bool, str]:
|
||||
"""La matrice d'audit des flux dit ce que le moteur declare AUJOURD'HUI.
|
||||
|
||||
|
|
@ -2000,12 +1792,6 @@ PREUVES: list[dict] = [
|
|||
"refs": [], "func": preuve_carte_dit_vrai},
|
||||
{"id": "P49", "titre": "Registre des flux : la matrice d'audit est a jour",
|
||||
"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": "P52", "titre": "Materialiser n'exige pas d'entrer dans le tenant",
|
||||
"refs": [], "func": preuve_materialiser_n_exige_pas_d_entrer},
|
||||
{"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": [],
|
||||
|
|
|
|||
Loading…
Reference in a new issue