reconstruction : quatre defauts que seule une flotte rasee pouvait montrer
Some checks are pending
verifier / verifier (push) Waiting to run
Some checks are pending
verifier / verifier (push) Waiting to run
Chezlepro detruite (15 VM, disques compris) et refaite depuis le gabarit minimal. 15/15 hotes, 0 echec ; make valider passe, test de restitution compris. Aucun defaut ne venait de la flotte ni du gabarit. 1. Un avertissement n est pas un echec. Proxmox rend WARNINGS: n pour une tache ABOUTIE ; la garde n acceptait que OK et declarait perdues cinq VM clonees a 100 pourcent. Le message parlait d etat stopped — celui de la TACHE, pas de la VM. 2. client_artefacts se contredisait : son commentaire disait de degrader, son code arretait. L autorite monte en premier, donc avant le cache du locataire : aucun ordre ne pouvait satisfaire la garde. 3. harden-below-nxdomain etendait le NXDOMAIN signe de la racine pour le TLD internal a toute la zone du site, sans jamais interroger l autoritatif. Declencheur : toute question sur un nom absent sous internal, y compris la zone d un autre locataire. Le cache contenait la bonne reponse ET un message negatif ; c est le negatif qui etait servi. aggressive-nsec: no avait semble marcher — c est le redemarrage qui vidait le cache, pas le reglage. 4. Un locataire doit savoir a qui demander la zone de son hebergeur, sans quoi il ne peut plus nommer son depot de sauvegarde. La derivation prenait dns_amorcage pour le resolveur du site : faux chez Technolibre, dont l amorcage est 9.9.9.9. P03 l a attrape avant tout deploiement. Au passage : instancier tentait encore le mot de passe unique d avant la separation des voutes ; comparer echouait en exit 4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
This commit is contained in:
parent
3ea0c95492
commit
0854b2a93c
8 changed files with 425 additions and 21 deletions
99
CHANGELOG.md
99
CHANGELOG.md
|
|
@ -1,5 +1,104 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-02 — Reconstruction de Chezlepro depuis zero : quatre defauts que seule une flotte rasee pouvait montrer
|
||||
|
||||
**56 preuves. `make valider` : 0 echec sur 13 hotes, test de restitution compris.**
|
||||
|
||||
Les quinze VM de Chezlepro ont ete detruites, disques compris, puis refaites depuis le
|
||||
gabarit minimal — `make reconstruire` : creation des VM, AC et DNS montes completement
|
||||
d'abord, puis toute la flotte par couches. Resultat : **15/15 hotes, 0 echec, 0
|
||||
injoignable**, et les neuf detenteurs d'etat redeposent sur le depot du site.
|
||||
|
||||
Aucun des quatre defauts rencontres ne venait de la flotte ni du gabarit. Tous venaient de
|
||||
gardes ou de derivations que rien n'avait jamais eprouvees, faute d'avoir jamais tout
|
||||
reconstruit.
|
||||
|
||||
### 1. Un avertissement n'est pas un echec
|
||||
|
||||
Cinq VM sur quinze declarees perdues. Le journal Proxmox disait pourtant
|
||||
`transferred 16.0 GiB of 16.0 GiB (100.00%)`, suivi de :
|
||||
|
||||
can't deactivate LV 'vm-9006-disk-1': Logical volume in use.
|
||||
WARN: volume deactivation failed
|
||||
|
||||
Proxmox desactive le volume DU GABARIT apres un clonage, et n'y arrive pas tant qu'un
|
||||
autre clonage parallele s'en sert — la consequence NORMALE de cloner un meme gabarit en
|
||||
parallele, ce que `flotte-creer` fait justement pour aller vite. La garde n'acceptait que
|
||||
`exitstatus == 'OK'`, or Proxmox rend trois formes : `OK`, `WARNINGS: n` pour une tache
|
||||
ABOUTIE qui signale quelque chose, et un texte d'erreur pour un vrai echec.
|
||||
|
||||
Son message trompait en plus : « etat *stopped* » designe l'etat de la TACHE (terminee),
|
||||
pas celui de la VM. On cherche un probleme de demarrage qui n'existe pas. Le message le
|
||||
dit desormais. Et l'avertissement est AFFICHE au lieu d'etre avale.
|
||||
|
||||
### 2. Une garde qui se contredisait elle-meme
|
||||
|
||||
`_amorcer-socle` monte l'autorite en premier, comme il se doit. `infra-pki-01` est donc la
|
||||
premiere machine debout — a un moment ou le cache du locataire, qui vit DANS la flotte
|
||||
qu'on reconstruit, n'existe pas encore. `client_artefacts` arretait tout la.
|
||||
|
||||
Le plus parlant : **le commentaire du role decrivait deja le bon comportement** — « en
|
||||
laissant le plancher en place, elles restent servies par le site : degrade, mais debout,
|
||||
et reparable par un deploiement ». Le code faisait une `assert` qui arretait. Or ce que la
|
||||
garde protege, c'est le plancher d'amorcage : il suffit de NE PAS l'ecraser. Arreter en
|
||||
plus ne protege rien et rend la reconstruction impossible — aucun ordre de deploiement ne
|
||||
pouvait la satisfaire, la contradiction etait dans la garde.
|
||||
|
||||
Elle degrade desormais, et le journal le DIT. La convergence a fonctionne dans la meme
|
||||
passe : les quinze machines ont fini sur le cache de leur ecosysteme.
|
||||
|
||||
### 3. La racine nie notre TLD, et Unbound etend ce « non » — la vraie cause
|
||||
|
||||
`internal.` n'est pas delegue dans la racine, qui est SIGNEE : elle rend une preuve
|
||||
NXDOMAIN validee pour ce TLD. `harden-below-nxdomain` (actif par defaut) tient ce « non »
|
||||
pour prouve et repond NXDOMAIN pour TOUT nom sous `internal.` DEPUIS SON CACHE — sans
|
||||
jamais interroger la `stub-zone` ni la `forward-zone`.
|
||||
|
||||
Le declencheur : n'importe quelle question sur un nom inexistant sous `internal.`, y
|
||||
compris la zone d'UN AUTRE ecosysteme, que ce resolveur ne sert pas et va donc chercher a
|
||||
la racine. Sur un resolveur partage par plusieurs locataires, ca arrive en permanence.
|
||||
|
||||
**Pourquoi ca a pris deux jours.** La panne parait intermittente : au redemarrage le cache
|
||||
est vide, tout fonctionne, on conclut que c'est regle. Une seule requete l'eteint ensuite
|
||||
pour des heures. Pire, le cache contenait EN MEME TEMPS la bonne reponse et un message
|
||||
negatif pour le meme nom — c'est le message qui etait servi. Il a fallu lire le cache.
|
||||
|
||||
Le remede a d'abord ete mal identifie : `aggressive-nsec: no` avait semble marcher, parce
|
||||
que le REDEMARRAGE qu'il imposait vidait le cache. C'est le vidage qui soignait. Mesure
|
||||
qui tranche, cache vide puis une requete empoisonnante :
|
||||
|
||||
harden-below-nxdomain: no seul -> repond
|
||||
aggressive-nsec: no seul -> ne repond pas
|
||||
|
||||
Ce qu'on perd : une protection anti-usurpation qui suppose que la racine dit vrai sur nos
|
||||
noms. Elle ne le peut pas — nos zones n'y sont pas.
|
||||
|
||||
### 4. Un locataire doit savoir a qui demander la zone de son hebergeur
|
||||
|
||||
Un locataire depend de services du SITE par leur NOM : cache, forge, depot de sauvegarde,
|
||||
autorite. Tant que ses machines pointaient sur le resolveur du site, ca marchait — par
|
||||
accident. Reconstruites proprement, elles utilisent LEUR resolveur, qui ignorait cette
|
||||
zone : `make valider` a echoue sur la RESTITUTION d'une sauvegarde. La sauvegarde etait
|
||||
intacte ; c'est le chemin pour la NOMMER qui manquait.
|
||||
|
||||
D'ou `serveur_resolveur_zones_deleguees`, derive par `instancier`. **La premiere version
|
||||
prenait `dns_amorcage` pour l'adresse du resolveur du site** — vrai chez Chezlepro, FAUX
|
||||
chez Technolibre, dont l'amorcage pointe sur `9.9.9.9`. On aurait delegue la zone
|
||||
souveraine de l'hebergeur a Quad9. **P03 l'a attrape avant tout deploiement**, en
|
||||
comparant l'inventaire de CHAQUE instance a son plan. L'adresse vient desormais du plan du
|
||||
site : la machine qui y porte `serveur_resolveur`.
|
||||
|
||||
Vide par defaut, et c'est le comportement d'un EMANCIPE : sans la carte de l'hebergeur, on
|
||||
ne delegue plus rien, donc on ne nomme plus ses services. C'est ce qu'on veut CONSTATER
|
||||
d'une emancipation, pas une panne a reparer.
|
||||
|
||||
### Au passage
|
||||
|
||||
`instancier` tentait encore `~/.config/setops-vault-pass`, le mot de passe UNIQUE d'avant
|
||||
la separation des voutes du 2026-08-28 : `comparer` echouait en exit 4 sur un message qui
|
||||
ne parlait que d'`ansible-inventory`. On ne pouvait donc plus voir ce qu'on changeait
|
||||
avant de l'appliquer. Il derive desormais ses identites de `voutes.py`.
|
||||
|
||||
## 2026-09-01 — Le depot de sauvegarde du SITE : l'etat d'un locataire quitte enfin sa propre flotte
|
||||
|
||||
**56 preuves.** Chezlepro rangeait ses instantanes sur `backup-01`, une VM DE SA PROPRE
|
||||
|
|
|
|||
92
docs/audit/preuve-2026-09-02.md
Normal file
92
docs/audit/preuve-2026-09-02.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-09-02
|
||||
|
||||
> 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 (56 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 : 40 groupes classes, aucun cycle, aucune arete en arriere. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 38 rôles, 98 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_resolveur_site |
|
||||
| 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 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. |
|
||||
| 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 : 12 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), 51 groupe(s), 81 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 | 32 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 21, 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 | 54 scripts expliques et atteignables, 108 cibles make documentees, 64 roles avec README. |
|
||||
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 41 exigence(s) de role, toutes satisfaites (131 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 (30 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 : 39 role(s) serveur/client tous nommes, 40 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 : 50 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 : 6 machine(s) du plan retrouvees, 80 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 (116 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 145 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. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 15 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 10 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 15 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 15. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
|
||||
## 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-09-02._
|
||||
|
|
@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
|
|||
| rôles | 64 | `roles/*/` |
|
||||
| README de rôles | 64 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
|
||||
| documents | 38 | `docs/*.md` |
|
||||
| pièces d'audit | 32 | `docs/audit/*` |
|
||||
| pièces d'audit | 33 | `docs/audit/*` |
|
||||
| unités de wiki | 27 | `wiki/*.md` |
|
||||
| décisions en vigueur | 79 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
|
||||
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
|
||||
|
|
|
|||
|
|
@ -371,12 +371,48 @@
|
|||
- not proxmox_clone_deja_la
|
||||
- proxmox_clone_lance.json.data is defined
|
||||
|
||||
# UN AVERTISSEMENT N'EST PAS UN ECHEC — ET IL NE DOIT PAS DISPARAITRE NON PLUS.
|
||||
#
|
||||
# Proxmox rend trois formes d'`exitstatus` : `OK`, `WARNINGS: <n>` pour une tache
|
||||
# QUI A ABOUTI en signalant quelque chose, et un texte d'erreur pour un vrai echec.
|
||||
# La garde n'acceptait que `OK` : elle declarait donc perdus des clonages termines.
|
||||
#
|
||||
# MESURE DU 2026-09-01, RECONSTRUCTION DE CHEZLEPRO. Cinq VM sur quinze refusees,
|
||||
# toutes avec `WARNINGS: 1`. Le journal de la tache disait :
|
||||
#
|
||||
# transferred 16.0 GiB of 16.0 GiB (100.00%)
|
||||
# can't deactivate LV 'vm-9006-disk-1': Logical volume in use.
|
||||
# WARN: volume deactivation failed
|
||||
#
|
||||
# Le disque etait transfere INTEGRALEMENT. Proxmox essaie ensuite de desactiver le
|
||||
# volume DU GABARIT, et n'y parvient pas tant qu'un autre clonage parallele s'en
|
||||
# sert. C'est la consequence NORMALE de cloner un meme gabarit en parallele — la
|
||||
# condition meme que `flotte-creer` recherche pour aller vite.
|
||||
#
|
||||
# Ce que ca coutait : la reconstruction s'arretait sur cinq VM parfaitement clonees,
|
||||
# et le message parlait d'« etat stopped » — l'etat de la TACHE, qui signifie
|
||||
# « terminee ». On lit « la VM est arretee » et on cherche un probleme de demarrage
|
||||
# qui n'existe pas. Le message dit desormais de quoi il parle.
|
||||
- name: Le clonage a-t-il signale quelque chose en aboutissant ?
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
Clonage de {{ proxmox_clone_nom }} abouti AVEC AVERTISSEMENT
|
||||
({{ proxmox_clone_tache.json.data.exitstatus }}). Le plus courant est
|
||||
« volume deactivation failed » sur le disque du gabarit : un autre clonage
|
||||
parallele s'en servait encore. Le journal complet de la tache est sur
|
||||
« {{ proxmox_clone_lance.json.data.split(':')[1] | default('?') }} ».
|
||||
when:
|
||||
- not proxmox_clone_deja_la
|
||||
- (proxmox_clone_tache.json.data.status | default('')) == 'stopped'
|
||||
- (proxmox_clone_tache.json.data.exitstatus | default('')) is match('WARNINGS')
|
||||
|
||||
- name: Dire si le clonage ne s'est pas termine correctement
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Le clonage de {{ proxmox_clone_nom }} (VMID {{ proxmox_clone_vmid }}) ne s'est pas
|
||||
termine correctement : etat « {{ proxmox_clone_tache.json.data.status | default('?')
|
||||
}} », sortie « {{ proxmox_clone_tache.json.data.exitstatus | default('?') }} ».
|
||||
termine correctement : la TACHE est « {{ proxmox_clone_tache.json.data.status
|
||||
| default('?') }} » (« stopped » = terminee, ce n'est pas l'etat de la VM) et sa
|
||||
sortie est « {{ proxmox_clone_tache.json.data.exitstatus | default('?') }} ».
|
||||
Regarder la tache sur
|
||||
« {{ proxmox_clone_lance.json.data.split(':')[1] | default('?') }} ».
|
||||
when:
|
||||
|
|
@ -386,9 +422,13 @@
|
|||
# clonage inter-noeuds interroge sur le mauvais noeud — il n'y avait aucun
|
||||
# `json.data`, donc aucun `exitstatus`, donc « OK » par defaut. On exige
|
||||
# desormais d'avoir VU la tache s'arreter, et bien s'arreter.
|
||||
# `OK` et `WARNINGS: <n>` disent tous deux que la tache a ABOUTI. Tout autre
|
||||
# texte est un vrai echec, et l'absence de sortie signifie qu'on n'a jamais vu
|
||||
# la tache s'arreter — le cas que la note ci-dessus decrit.
|
||||
- >-
|
||||
(proxmox_clone_tache.json.data.status | default('')) != 'stopped'
|
||||
or (proxmox_clone_tache.json.data.exitstatus | default('')) != 'OK'
|
||||
or not ((proxmox_clone_tache.json.data.exitstatus | default('')) == 'OK'
|
||||
or (proxmox_clone_tache.json.data.exitstatus | default('')) is match('WARNINGS'))
|
||||
|
||||
- name: Dire pourquoi le clonage a echoue, s'il a echoue
|
||||
ansible.builtin.fail:
|
||||
|
|
|
|||
|
|
@ -41,19 +41,39 @@
|
|||
- client_artefacts_actif | bool
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Refuser de detourner apt vers une source muette
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- client_artefacts_sonde is not failed
|
||||
fail_msg: >-
|
||||
{{ client_artefacts_hote }}:{{ client_artefacts_port }} ne repond pas. Diriger `apt`
|
||||
vers elle retirerait a cette machine le cache d'amorcage du SITE, qui fonctionne —
|
||||
et la flotte perdrait `apt` sans qu'un deploiement puisse la reparer.
|
||||
Deployer `serveur_artefacts` sur {{ client_artefacts_hote }} d'abord.
|
||||
success_msg: "Source d'artefacts {{ client_artefacts_hote }} joignable."
|
||||
# DEGRADER, PAS ARRETER — le commentaire ci-dessus disait deja quoi faire, le code
|
||||
# faisait autre chose (mesure du 2026-09-01, reconstruction de Chezlepro depuis zero).
|
||||
#
|
||||
# La garde etait une `assert` qui ARRETAIT le deploiement. Or ce qu'elle protege, c'est
|
||||
# le plancher d'amorcage du site : il suffit de NE PAS l'ecraser. Arreter en plus, ca ne
|
||||
# protege rien de plus, et ca rend la reconstruction depuis zero IMPOSSIBLE.
|
||||
#
|
||||
# CE QUE CA A COUTE : `_amorcer-socle` monte l'autorite en premier, comme il se doit.
|
||||
# `infra-pki-01` est donc la premiere machine debout — a un moment ou le cache du
|
||||
# locataire, qui vit DANS la flotte qu'on reconstruit, n'existe evidemment pas encore.
|
||||
# La garde tirait sur la premiere machine de la premiere couche, et tout s'arretait la.
|
||||
# Aucun ordre de deploiement ne pouvait la satisfaire : la contradiction etait dans la
|
||||
# garde, pas dans le plan.
|
||||
#
|
||||
# Le comportement juste est la CONVERGENCE : tant que la source du locataire est muette,
|
||||
# la machine reste servie par le site — degradee, debout, et reparable par le passage
|
||||
# suivant. Quand `serveur_artefacts` sera monte sur son hote, le deploiement d'apres
|
||||
# basculera apt de lui-meme.
|
||||
#
|
||||
# On le DIT, fort : une machine qui reste sur le cache du site alors qu'elle devrait
|
||||
# etre passee a celui de son ecosysteme est un ecart qu'il faut pouvoir lire dans le
|
||||
# journal, pas deviner.
|
||||
- name: Rester sur le cache du SITE tant que celui de l'écosystème est muet
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ client_artefacts_hote }}:{{ client_artefacts_port }} ne repond pas — apt N'EST
|
||||
PAS detourne. Cette machine reste servie par le cache d'amorcage du SITE, qui
|
||||
fonctionne : degrade, debout, reparable. La bascule se fera au deploiement suivant,
|
||||
une fois `serveur_artefacts` monte sur {{ client_artefacts_hote }}.
|
||||
when:
|
||||
- client_artefacts_actif | bool
|
||||
- not ansible_check_mode
|
||||
- client_artefacts_sonde is failed
|
||||
|
||||
- name: Diriger apt vers la source de l'écosystème
|
||||
ansible.builtin.copy:
|
||||
|
|
@ -77,7 +97,12 @@
|
|||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
when: client_artefacts_actif | bool
|
||||
when:
|
||||
- client_artefacts_actif | bool
|
||||
# Ne jamais ecraser le plancher du site par une source qu'on n'a pas VU repondre.
|
||||
# En `check_mode` la sonde ne tourne pas : `is failed` est alors faux, et on garde
|
||||
# le comportement de simulation d'avant.
|
||||
- not (client_artefacts_sonde is failed)
|
||||
|
||||
# L'écosystème qui RETIRE sa source d'artefacts doit voir ses hôtes revenir à l'amont,
|
||||
# sans quoi ils resteraient braqués sur une machine disparue et n'installeraient plus
|
||||
|
|
|
|||
|
|
@ -59,3 +59,19 @@ serveur_resolveur_zones_inverses: >-
|
|||
# reviendrait à confier chaque question de l'écosystème à un tiers — ce que la bascule du
|
||||
# 2026-08-23 avait justement supprimé.
|
||||
serveur_resolveur_transitaires: []
|
||||
|
||||
# LES ZONES QUI NE SONT PAS À NOUS, ET À QUI LES DEMANDER.
|
||||
#
|
||||
# Liste de `{nom, adresses}`. Un locataire y trouve la zone de son HÉBERGEUR : il dépend
|
||||
# de ses services par leur NOM — cache d'artefacts, forge, dépôt de sauvegarde, autorité
|
||||
# — et doit savoir à quel résolveur les demander.
|
||||
#
|
||||
# DÉRIVÉE, PAS DÉCLARÉE : `inventory_host.py` la construit depuis la carte de
|
||||
# l'hébergeur (le symlink `underlay.yml`) et l'adresse d'amorçage DNS que le locataire
|
||||
# connaît déjà. Deux déclarations du même fait finiraient par diverger.
|
||||
#
|
||||
# VIDE PAR DÉFAUT, ET C'EST LE COMPORTEMENT D'UN ÉMANCIPÉ : un écosystème dont la carte
|
||||
# de l'hébergeur n'est plus montée ne délègue plus rien. Il ne peut alors plus nommer les
|
||||
# services du site — ce qui est exactement ce qu'on veut constater d'une émancipation, et
|
||||
# non une panne à réparer.
|
||||
serveur_resolveur_zones_deleguees: []
|
||||
|
|
|
|||
|
|
@ -11,6 +11,37 @@ server:
|
|||
hide-version: yes
|
||||
prefetch: yes
|
||||
do-ip6: no
|
||||
# LA RACINE PROUVE QUE NOTRE TLD N'EXISTE PAS, ET UNBOUND ETEND CE « NON »
|
||||
# A TOUT CE QUI EST DESSOUS (mesure du 2026-09-02).
|
||||
#
|
||||
# `internal.` n'est pas delegue dans la racine, qui est SIGNEE : elle rend donc une
|
||||
# preuve NXDOMAIN validee pour ce TLD. `harden-below-nxdomain` (actif par defaut)
|
||||
# tient ce « non » pour prouve et repond NXDOMAIN pour TOUT nom sous `internal.`
|
||||
# DEPUIS SON CACHE — sans jamais interroger la `stub-zone` ni la `forward-zone`
|
||||
# declarees plus bas. La delegation etait correcte, l'autoritatif repondait juste, et
|
||||
# pas une requete ne lui parvenait.
|
||||
#
|
||||
# CE QUI DECLENCHE L'EMPOISONNEMENT : n'importe quelle question sur un nom inexistant
|
||||
# sous `internal.` — y compris la zone d'UN AUTRE ecosysteme, que ce resolveur ne
|
||||
# sert pas et va donc chercher a la racine. Sur un resolveur partage par plusieurs
|
||||
# locataires, ca arrive en permanence.
|
||||
#
|
||||
# POURQUOI LE DIAGNOSTIC A DEMANDE DEUX JOURS. La panne parait INTERMITTENTE : au
|
||||
# redemarrage le cache est vide, tout fonctionne, on conclut que c'est regle. Puis
|
||||
# une seule requete l'eteint pour des heures. Pire, le cache contenait EN MEME TEMPS
|
||||
# la bonne reponse (`A 10.0.35.11`) et un message negatif pour le meme nom — c'est le
|
||||
# message qui etait servi. Il a fallu lire le cache pour le voir.
|
||||
#
|
||||
# MESURE QUI TRANCHE (trois essais, cache vide puis une requete empoisonnante) :
|
||||
# harden-below-nxdomain: no seul -> repond 10.0.35.11
|
||||
# aggressive-nsec: no seul -> ne repond pas
|
||||
# Le second avait ete essaye d'abord et avait semble marcher : le REDEMARRAGE qu'il
|
||||
# imposait vidait le cache. C'est le vidage qui soignait, pas le reglage.
|
||||
#
|
||||
# Ce qu'on perd : une protection anti-usurpation qui suppose que la racine dit vrai
|
||||
# sur nos noms. Elle ne le peut pas — nos zones ne sont pas dans la racine. Ce qu'on
|
||||
# garde : la validation DNSSEC complete de tout ce qui, lui, y est delegue.
|
||||
harden-below-nxdomain: no
|
||||
# La zone souveraine n'est pas signée : on ne la soumet pas à la validation DNSSEC.
|
||||
domain-insecure: "{{ serveur_resolveur_zone_interne }}"
|
||||
# LA RACINE NIE NOTRE TLD, ET UNBOUND ÉTEND CE NON À TOUT CE QUI EST DESSOUS
|
||||
|
|
@ -69,6 +100,26 @@ server:
|
|||
# flotte, si bien qu'un `getent hosts forge.genese.internal` semblait prouver que la
|
||||
# résolution marchait. Seul un `dig` explicite l'a montrée.
|
||||
do-not-query-localhost: no
|
||||
{% for d in serveur_resolveur_zones_deleguees | default([]) %}
|
||||
# LA ZONE D'UN AUTRE — CELLE DE L'HÉBERGEUR (2026-09-02).
|
||||
#
|
||||
# Un locataire dépend de services du SITE par leur NOM : son cache d'artefacts, sa
|
||||
# forge, son dépôt de sauvegarde, son autorité. Ces noms vivent dans une zone que ce
|
||||
# résolveur ne sert pas et n'a pas à servir — il doit savoir À QUI les demander.
|
||||
#
|
||||
# CE QUE SON ABSENCE A COÛTÉ : tant que les machines du locataire pointaient
|
||||
# directement sur le résolveur du site, ça marchait — par accident. Reconstruites
|
||||
# proprement, elles utilisent LEUR résolveur, qui ignorait la zone de l'hébergeur :
|
||||
# la recette de validation a échoué sur la RESTITUTION d'une sauvegarde, avec
|
||||
# « Could not resolve hostname sauvegarde.genese.internal ». La sauvegarde était
|
||||
# intacte ; c'est le chemin pour la nommer qui manquait.
|
||||
#
|
||||
# Mêmes deux lignes que pour notre propre zone, même cause : la racine rend un
|
||||
# NXDOMAIN signé pour le TLD, et `harden-below-nxdomain` l'étendrait à toute la zone
|
||||
# sans jamais interroger le transitaire déclaré plus bas.
|
||||
domain-insecure: "{{ d.nom }}"
|
||||
local-zone: "{{ d.nom }}." transparent
|
||||
{% endfor %}
|
||||
|
||||
stub-zone:
|
||||
name: "{{ serveur_resolveur_zone_interne }}"
|
||||
|
|
@ -83,6 +134,18 @@ stub-zone:
|
|||
name: "{{ zone_inverse }}"
|
||||
stub-addr: {{ serveur_resolveur_autoritatif }}@{{ serveur_resolveur_autoritatif_port }}
|
||||
{% endfor %}
|
||||
{% for d in serveur_resolveur_zones_deleguees | default([]) %}
|
||||
|
||||
{# `forward-zone` ET NON `stub-zone` : on s'adresse au RÉSOLVEUR de l'hébergeur, qui
|
||||
recursera pour nous, et non à son autoritatif — que rien ne nous autorise à joindre
|
||||
et dont l'adresse ne nous regarde pas. Le locataire connaît une seule adresse du
|
||||
site en matière de noms : celle par laquelle il a été amorcé. #}
|
||||
forward-zone:
|
||||
name: "{{ d.nom }}"
|
||||
{% for a in d.adresses %}
|
||||
forward-addr: {{ a }}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if serveur_resolveur_transitaires %}
|
||||
|
||||
forward-zone:
|
||||
|
|
|
|||
|
|
@ -85,6 +85,54 @@ def _domaine_interne() -> str:
|
|||
|
||||
|
||||
|
||||
def _zones_deleguees() -> list:
|
||||
"""La zone de l'HEBERGEUR, et l'adresse a qui la demander. [] si aucune carte.
|
||||
|
||||
POURQUOI CETTE DERIVATION EXISTE (mesure du 2026-09-02, reconstruction de Chezlepro
|
||||
depuis zero). Un locataire depend de services du SITE par leur NOM : son cache
|
||||
d'artefacts, sa forge, son depot de sauvegarde, son autorite. Ces noms vivent dans une
|
||||
zone que SON resolveur ne sert pas.
|
||||
|
||||
Tant que ses machines pointaient directement sur le resolveur du site, ca marchait —
|
||||
par accident. Reconstruites proprement, elles utilisent LEUR resolveur, qui ignorait
|
||||
la zone de l'hebergeur : la recette a echoue sur la RESTITUTION d'une sauvegarde, avec
|
||||
« Could not resolve hostname sauvegarde.genese.internal ». La sauvegarde etait
|
||||
intacte ; c'est le chemin pour la NOMMER qui manquait.
|
||||
|
||||
DERIVEE DU PLAN DU SITE, PAS DE `dns_amorcage` — et la nuance a failli couter cher.
|
||||
La premiere version prenait `dns_amorcage` pour « l'adresse du resolveur du site ».
|
||||
C'est vrai chez Chezlepro ; c'est FAUX chez Technolibre, dont l'amorcage pointe sur
|
||||
`9.9.9.9,149.112.112.112`. On aurait delegue la zone souveraine de l'hebergeur a
|
||||
Quad9, qui n'en sait rien — et le NXDOMAIN rendu aurait ressemble a une zone vide.
|
||||
P03 l'a attrape avant tout deploiement, en comparant l'inventaire de CHAQUE instance
|
||||
a son plan.
|
||||
#
|
||||
L'adresse vient donc du PLAN DU SITE : la machine qui y porte `serveur_resolveur`.
|
||||
C'est la seule source qui dise, par construction, qui resout la zone du site.
|
||||
|
||||
REND [] QUAND LA CARTE N'EST PAS MONTEE, et c'est le comportement d'un EMANCIPE : il
|
||||
ne delegue plus rien, donc ne peut plus nommer les services du site. C'est ce qu'on
|
||||
veut CONSTATER d'une emancipation, pas une panne a reparer.
|
||||
"""
|
||||
try:
|
||||
import underlay as underlay_mod
|
||||
zone = str((underlay_mod.lire_plan_site("10-intrants.yml") or {})
|
||||
.get("domaine_interne") or "").strip()
|
||||
except Exception:
|
||||
return []
|
||||
# Un site qui s'heberge lui-meme partage la zone de son instance : il n'y a alors
|
||||
# rien a deleguer, et se declarer transitaire de soi-meme ferait une boucle.
|
||||
if not zone or zone == _domaine_interne():
|
||||
return []
|
||||
try:
|
||||
adresses = [a for a in underlay_mod.adresses_site_portant("serveur_resolveur") if a]
|
||||
except Exception:
|
||||
adresses = []
|
||||
if not adresses:
|
||||
return []
|
||||
return [{"nom": zone, "adresses": adresses}]
|
||||
|
||||
|
||||
def _attributs_cible(vers: str, apps: dict, serveurs: dict, nomenclature: dict, domaine: str) -> dict | None:
|
||||
"""Resout une cible de lien en attributs substituables. Phase 1 : applications."""
|
||||
if vers in apps:
|
||||
|
|
@ -213,6 +261,7 @@ def generer() -> dict:
|
|||
+ "".join(f" - {n} : fonction « {serveurs[n].get('fonction', '')} »\n" for n in sans_fonction)
|
||||
+ f"Fonctions connues de ce plan : {connues}")
|
||||
|
||||
zones_deleguees = _zones_deleguees()
|
||||
children: dict = {
|
||||
"modeles_vm": {"hosts": {}},
|
||||
"hotes_actifs": {"hosts": {}},
|
||||
|
|
@ -261,6 +310,9 @@ def generer() -> dict:
|
|||
sans = sorted({e["fqdn"] for e in expositions if e.get("edge") in groupes})
|
||||
if sans:
|
||||
hostvars["sans_exposition"] = sans
|
||||
# Le resolveur de l'ecosysteme doit savoir a qui demander la zone de l'hebergeur.
|
||||
if "serveur_resolveur" in groupes and zones_deleguees:
|
||||
hostvars["serveur_resolveur_zones_deleguees"] = zones_deleguees
|
||||
etat = "hotes_actifs" if srv.get("etat") == "actif" else "hotes_planifies"
|
||||
children[etat]["hosts"][nom] = hostvars
|
||||
for groupe in sorted(groupes):
|
||||
|
|
@ -280,12 +332,29 @@ def ecrire(path: Path = GENERE) -> None:
|
|||
def _resolu(fichier: Path) -> tuple[dict, dict]:
|
||||
cmd = ["ansible-inventory", "-i", str(fichier), "--list"]
|
||||
# Une voute chiffree (group_vars/all/vault.yml) fait echouer ansible-inventory sans
|
||||
# mot de passe (exit 4). Si ANSIBLE_VAULT_PASSWORD_FILE n'est pas deja fourni, on
|
||||
# tente le fichier conventionnel Set-OPS ~/.config/setops-vault-pass.
|
||||
if not os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE"):
|
||||
conv = Path.home() / ".config" / "setops-vault-pass"
|
||||
if conv.is_file():
|
||||
cmd += ["--vault-password-file", str(conv)]
|
||||
# mot de passe (exit 4).
|
||||
#
|
||||
# UNE VOUTE, UNE CLE (depuis le 2026-08-28). Ce bloc tentait
|
||||
# `~/.config/setops-vault-pass` — le mot de passe UNIQUE d'avant la separation, qui
|
||||
# n'ouvre plus rien. `instancier comparer` echouait donc en exit 4 sur un message qui
|
||||
# ne parlait que d'`ansible-inventory` : on ne pouvait plus relire l'inventaire en
|
||||
# place pour le comparer au genere, donc plus l'appliquer sans `--force`, donc plus
|
||||
# voir ce qu'on changeait.
|
||||
#
|
||||
# `voutes.py identites` rend la liste `etiquette@chemin` de toutes les voutes
|
||||
# connues ; Ansible essaie chacune et retient celle qui ouvre. On ne pose rien si
|
||||
# l'appelant a deja renseigne l'environnement : sa valeur est plus precise que la
|
||||
# notre.
|
||||
if not os.environ.get("ANSIBLE_VAULT_PASSWORD_FILE") \
|
||||
and not os.environ.get("ANSIBLE_VAULT_IDENTITY_LIST"):
|
||||
try:
|
||||
ident = subprocess.run(
|
||||
[sys.executable, str(Path(__file__).parent / "voutes.py"), "identites"],
|
||||
capture_output=True, text=True, check=True).stdout.strip()
|
||||
except (subprocess.CalledProcessError, OSError):
|
||||
ident = ""
|
||||
if ident:
|
||||
os.environ["ANSIBLE_VAULT_IDENTITY_LIST"] = ident
|
||||
sortie = subprocess.run(cmd, capture_output=True, text=True, check=True).stdout
|
||||
data = json.loads(sortie)
|
||||
hostvars = data.get("_meta", {}).get("hostvars", {})
|
||||
|
|
|
|||
Loading…
Reference in a new issue