mtu : le 1450 de la zone n'atteignait pas les invites
Question de l'exploitant : « je ne vois nulle part un MTU a 1450 ». Fondee. Le 1450 existait bien — MTU_OVERLAY_DEFAUT dans underlay.py, dont P23 derive sa garde (transport >= overlay + 50), pose par le devis SDN sur chaque zone, et verifie sur le cluster. Mais il ne se propage pas a la carte de l'invite : les 14 VM tournaient a 1500, et cloner_vm_debian.yml n'avait aucune occurrence de `mtu`. La VM emettait donc des trames que son propre chemin ne pouvait pas encapsuler — la panne que le registre des flux appelle « la plus couteuse a diagnostiquer ». Invisible parce que les 14 VM vivent sur asgard : deux VM du meme hyperviseur communiquent par le pont local, sans encapsulation. Le defaut serait apparu au premier eclatement de la flotte — c'est-a-dire quand il faudra heberger les deux tenants ensemble. Corrige en DEUX endroits, avec `mtu=1` (« herite du pont ») plutot que 1450 en dur : juste en SDN comme hors SDN, et encore juste le jour des trames jumbo. Le GABARIT le porte — proposition de l'exploitant, meilleure que la mienne : elle couvre les clonages qui ne passent pas par le playbook. Et le playbook le repose a chaque clone, parce qu'un gabarit se RECAPTURE et que ce qui n'est pas versionne se perd en silence. make mtu-mesurer (7e devis) rattache chaque hote a sa zone par son pont derive et lit le MTU attendu dans devis_sdn.py. Ecart trouve sur 14 hotes du premier coup, puis confirme corrige. ET J'AI CASSE LA FLOTTE EN CORRIGEANT : applique aux cartes de VM EN MARCHE, le changement detache et rebranche la carte sans que l'invite reconfigure son interface. 0/14 joignables pendant trois minutes. L'agent qemu, independant du reseau invite, a permis de constater et de redemarrer par l'API. Dans le playbook la meme tache s'execute AVANT le demarrage du clone : aucun risque. Sur une VM en service : poser la config, puis redemarrer — une seule d'abord. Verifie : mtu-mesurer CONFORME 14/14 a 1450, prouver.py 35 OK, ansible-lint production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
777bea8408
commit
6173d5bfe9
7 changed files with 260 additions and 1 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,5 +1,56 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-10 — Le MTU de la zone n'atteignait pas les invités
|
||||
|
||||
Question de l'exploitant : « je ne vois nulle part un MTU à 1450 ». Elle était fondée.
|
||||
|
||||
**Ce qui existait déjà** : `MTU_OVERLAY_DEFAUT = 1450` dans `scripts/underlay.py`, dont
|
||||
**P23** dérive sa garde (*transport ≥ overlay + 50*), et que le devis SDN pose sur chaque
|
||||
zone. Vérifié sur le cluster : zones `t11` et `t17` bien à **1450**.
|
||||
|
||||
**Ce qui manquait** : le MTU d'une zone ne se propage pas à la carte de l'invité. Les
|
||||
quatorze VM tournaient à **1500**, et `cloner_vm_debian.yml` ne contenait aucune occurrence
|
||||
de `mtu`. La VM émettait donc des trames que son propre chemin ne pouvait pas encapsuler —
|
||||
la connexion s'établit, les petites requêtes passent, les grosses réponses restent
|
||||
suspendues. C'est la panne que le registre des flux décrit comme « la plus coûteuse à
|
||||
diagnostiquer », et pour laquelle il déclare l'ICMP « fragmentation nécessaire ».
|
||||
|
||||
**Pourquoi personne ne l'avait vue** : les quatorze VM vivent sur `asgard`. Deux VM du même
|
||||
hyperviseur communiquent par le pont local, **sans encapsulation** — rien ne rencontre le
|
||||
1450. Le défaut serait apparu au premier éclatement de la flotte sur plusieurs nœuds, c'est-
|
||||
à-dire exactement quand il faudra héberger les deux tenants ensemble.
|
||||
|
||||
**Corrigé en deux endroits, et `mtu=1` plutôt que `1450`** — la valeur Proxmox qui signifie
|
||||
« hérite du pont » : juste en SDN (1450) comme hors SDN (1500), et encore juste le jour où la
|
||||
fabric passera aux trames jumbo.
|
||||
|
||||
- le **gabarit** le porte (`net0 … mtu=1`), ce qui couvre les clonages qui ne passent pas par
|
||||
le playbook — un clone fait à la main, par exemple. Proposition de l'exploitant, et elle
|
||||
est meilleure : elle attrape tous les chemins ;
|
||||
- `cloner_vm_debian.yml` le repose à chaque clone, parce qu'un gabarit **se recapture** (fait
|
||||
la veille) et que ce qui n'est pas versionné se perd en silence.
|
||||
|
||||
### `make mtu-mesurer` — septième devis
|
||||
|
||||
Il rattache chaque hôte à sa zone par son **pont dérivé** (`t17serv` → zone `t17`) et lit le
|
||||
MTU attendu dans `devis_sdn.py`, la source qui configure les zones. Rien n'est saisi. Il a
|
||||
trouvé l'écart sur 14 hôtes du premier coup, et l'a confirmé corrigé.
|
||||
|
||||
### Ce que j'ai cassé en corrigeant
|
||||
|
||||
Appliquer `mtu=1` aux cartes de VM **en marche** a coupé le réseau des **quatorze machines
|
||||
d'un coup** : Proxmox détache et rebranche la carte, l'invité ne reconfigure pas son
|
||||
interface. Flotte à 0/14 pendant trois minutes.
|
||||
|
||||
Ce qui a permis d'en sortir : les VM tournaient et l'**agent qemu répondait** — un canal
|
||||
indépendant du réseau invité. Redémarrage par l'API, la configuration s'est appliquée
|
||||
proprement au démarrage, 14/14 ensuite.
|
||||
|
||||
La faute est d'avoir appliqué à la flotte entière un changement dont je n'avais pas mesuré
|
||||
l'effet à chaud. Dans le playbook, la même tâche s'exécute **avant** le démarrage du clone :
|
||||
aucun risque. Sur une VM déjà en service : poser la configuration, **puis** redémarrer — et
|
||||
sur une seule d'abord.
|
||||
|
||||
## 2026-08-10 — Technolibre est debout : six devis, et P35
|
||||
|
||||
**L'épreuve de portabilité est passée.** Un second écosystème souverain complet, monté depuis
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -418,7 +418,7 @@ ca-empreinte: ansible-runtime _instance-requise ## Empreinte de la racine, lue S
|
|||
-a "step certificate fingerprint /etc/step-ca/certs/root_ca.crt" 2>/dev/null \
|
||||
| tail -1 | tr -d ' \r'
|
||||
|
||||
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer
|
||||
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer
|
||||
courriel-plan: ansible-runtime ## Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture)
|
||||
@rm -f $(SETOPS_INSTANCE)/devis-courriel.json.*
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-courriel.yml >/dev/null
|
||||
|
|
@ -433,6 +433,10 @@ expositions-plan: ansible-runtime ## Chaque exposition du plan repond-elle ? (ed
|
|||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-expositions.yml >/dev/null
|
||||
@python3 scripts/devis_expositions.py
|
||||
|
||||
mtu-mesurer: ansible-runtime ## L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture)
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-mtu.yml >/dev/null
|
||||
@python3 scripts/devis_mtu.py
|
||||
|
||||
frontiere-mesurer: ansible-runtime ## La frontiere refuse-t-elle ce qui n'est pas declare ? (sonde reelle, aucune ecriture)
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-frontiere.yml >/dev/null
|
||||
@python3 scripts/devis_frontiere.py
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ make expositions-plan # chaque `expose:` du plan répond-il, depuis l'edge et
|
|||
make postgresql-plan # chiffrement imposé, et à quels réseaux
|
||||
make courriel-plan # Postfix → LDAP → LMTP → Dovecot → IMAP
|
||||
make frontiere-mesurer # ce qui n'est pas déclaré à la frontière est-il refusé ?
|
||||
make mtu-mesurer # l'invité porte-t-il le MTU de sa zone SDN ?
|
||||
```
|
||||
|
||||
## Le trou qu'il comble
|
||||
|
|
|
|||
|
|
@ -33,6 +33,25 @@ aucun secret
|
|||
aucune donnée propre à un clone final
|
||||
```
|
||||
|
||||
### La carte réseau du gabarit porte `mtu=1`
|
||||
|
||||
`mtu=1` est la valeur Proxmox qui signifie **« hérite du pont »** (virtio uniquement) — et
|
||||
non « MTU de 1 octet ». Tout clone naît donc au MTU du pont auquel il est réellement
|
||||
attaché : **1450** sur une zone SDN EVPN (VXLAN coûte 50 octets), **1500** sur un pont
|
||||
classique.
|
||||
|
||||
Sans elle, l'invité naît à 1500 quel que soit le pont. La panne qui en découle est sournoise
|
||||
et **invisible tant que toutes les VM vivent sur le même hyperviseur** — elles communiquent
|
||||
alors par le pont local, sans encapsulation. Mesuré le 2026-08-10 : zones à 1450, les
|
||||
quatorze invités à 1500, aucun symptôme, parce que les quatorze étaient sur `asgard`.
|
||||
|
||||
Écrire `1450` en dur serait faux hors SDN ; hériter reste juste dans les deux mondes, et le
|
||||
jour où la fabric passera aux trames jumbo.
|
||||
|
||||
**Ce réglage se perd à chaque recapture du gabarit** — le vérifier fait partie de la
|
||||
recapture. `cloner_vm_debian.yml` le repose de toute façon à chaque clone, et
|
||||
`make mtu-mesurer` contrôle le résultat sur la flotte.
|
||||
|
||||
## Runbook du playbook prepare
|
||||
|
||||
### Objectif
|
||||
|
|
|
|||
60
playbooks/maintenance/devis-mtu.yml
Normal file
60
playbooks/maintenance/devis-mtu.yml
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
# Devis du MTU des invites — RELEVE seul. Ne modifie rien (D-23).
|
||||
#
|
||||
# Question posee : la carte d'une VM porte-t-elle le MTU de la zone a laquelle elle est
|
||||
# reellement attachee ?
|
||||
#
|
||||
# En SDN EVPN, VXLAN coute 50 octets : la zone est a 1450 quand le transport est a 1500.
|
||||
# Le MTU de la ZONE est pose par `make sdn-appliquer` cote hyperviseur — mais l'invite,
|
||||
# lui, nait a 1500 si sa carte ne l'herite pas explicitement (`mtu=1` chez Proxmox).
|
||||
#
|
||||
# CE QUE CA PRODUIT QUAND C'EST FAUX, et pourquoi ca merite une garde : la VM emet des
|
||||
# trames que son propre chemin ne peut pas encapsuler. La connexion s'etablit, les petites
|
||||
# requetes passent, les grosses reponses restent suspendues — le registre des flux la
|
||||
# decrit comme « la panne la plus couteuse a diagnostiquer », et c'est pour elle qu'il
|
||||
# declare l'ICMP « fragmentation necessaire ».
|
||||
#
|
||||
# INVISIBLE TANT QUE TOUTES LES VM VIVENT SUR LE MEME HYPERVISEUR : elles communiquent
|
||||
# alors par le pont local, sans encapsulation, et rien ne rencontre le 1450. Mesure du
|
||||
# 2026-08-10 : zones a 1450, les 14 invites a 1500, aucun symptome — parce que les 14
|
||||
# etaient sur `asgard`.
|
||||
#
|
||||
# make mtu-mesurer
|
||||
|
||||
- name: Relevé du MTU vu par les invités
|
||||
hosts: hotes_actifs
|
||||
become: false
|
||||
gather_facts: false
|
||||
|
||||
tasks:
|
||||
- name: Lire le MTU de l'interface principale
|
||||
ansible.builtin.slurp:
|
||||
src: "/sys/class/net/{{ devis_mtu_interface | default('eth0') }}/mtu"
|
||||
register: devis_mtu_lu
|
||||
failed_when: false
|
||||
|
||||
- name: Retenir le MTU de cet hôte
|
||||
ansible.builtin.set_fact:
|
||||
devis_mtu_hote: >-
|
||||
{{ (devis_mtu_lu.content | default('') | b64decode | trim | int)
|
||||
if devis_mtu_lu.content is defined else 0 }}
|
||||
|
||||
- name: Déposer le relevé
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: false
|
||||
gather_facts: false
|
||||
|
||||
tasks:
|
||||
- name: Écrire le relevé sur le contrôleur
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ playbook_dir }}/../../instance/devis-mtu.json"
|
||||
mode: "0600"
|
||||
content: >-
|
||||
{{ {'invites': dict(groups['hotes_actifs']
|
||||
| map('extract', hostvars, 'devis_mtu_hote') | list
|
||||
| zip(groups['hotes_actifs']) | map('reverse') | list),
|
||||
'ponts': dict(groups['hotes_actifs']
|
||||
| map('extract', hostvars, 'proxmox_pont') | list
|
||||
| zip(groups['hotes_actifs']) | map('reverse') | list)}
|
||||
| to_nice_json }}
|
||||
|
|
@ -320,6 +320,36 @@
|
|||
bridge: "{{ proxmox_clone_pont }}"
|
||||
tag: "{{ proxmox_clone_vlan | int if proxmox_clone_vlan is defined and proxmox_clone_vlan | string | length > 0 else omit }}"
|
||||
firewall: "{{ proxmox_clone_parefeu_interface | default(false) | bool }}"
|
||||
# `mtu: 1` est la valeur Proxmox pour « HERITE DU PONT » (virtio uniquement).
|
||||
#
|
||||
# Sans elle, l'invite nait a 1500 quel que soit le pont. En SDN EVPN la zone est a
|
||||
# 1450 (VXLAN coute 50 octets) : la VM emet donc des trames que son propre chemin
|
||||
# ne peut pas encapsuler. Ca ne casse pas franchement — la connexion s'etablit,
|
||||
# les petites requetes passent, les grosses reponses restent suspendues. C'est
|
||||
# exactement la panne que le registre des flux decrit comme « la plus couteuse a
|
||||
# diagnostiquer », et pourquoi il declare l'ICMP « fragmentation necessaire ».
|
||||
#
|
||||
# Mesure du 2026-08-10 : zones `t11` et `t17` a 1450 sur le cluster, les 14 invites
|
||||
# a 1500. Invisible tant que toutes les VM vivent sur le MEME hyperviseur — elles
|
||||
# communiquent alors par le pont local, sans encapsulation. La panne apparaitrait
|
||||
# au premier eclatement de la flotte sur plusieurs noeuds.
|
||||
#
|
||||
# HERITER plutot qu'ecrire 1450 : hors SDN le pont est a 1500 et la VM suit. Une
|
||||
# seule source de verite — celle du pont auquel elle est reellement attachee — et
|
||||
# la valeur reste juste le jour ou la fabric passera aux trames jumbo.
|
||||
#
|
||||
# Le GABARIT le porte aussi (`net0 ... mtu=1`), ce qui couvre les clonages qui ne
|
||||
# passent pas par ce playbook — un clone fait a la main dans l'interface, par
|
||||
# exemple. On garde neanmoins la ligne ici : un gabarit se RECAPTURE (fait le
|
||||
# 2026-08-09), et ce qui n'est pas versionne se perd en silence. Le playbook est
|
||||
# la garantie qui survit a la recapture ; `make mtu-mesurer` verifie le resultat.
|
||||
#
|
||||
# Cette tache s'execute AVANT le demarrage du clone (voir « Demarrer le clone »
|
||||
# plus bas) : aucun risque de rebranchement a chaud. Applique a une VM EN MARCHE,
|
||||
# le meme changement detache et rebranche la carte sans que l'invite reconfigure
|
||||
# son interface — 14 machines coupees d'un coup le 2026-08-10, et il a fallu les
|
||||
# redemarrer. Sur une VM deja en service : poser la config, puis redemarrer.
|
||||
mtu: 1
|
||||
state: present
|
||||
when:
|
||||
- proxmox_clone_pont is defined
|
||||
|
|
|
|||
94
scripts/devis_mtu.py
Normal file
94
scripts/devis_mtu.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Devis du MTU : l'invite porte-t-il le MTU de la zone a laquelle il est attache ?
|
||||
|
||||
Le MTU de la ZONE est pose sur l'hyperviseur par `make sdn-appliquer`. Rien ne garantit
|
||||
que l'INVITE le porte : sa carte doit l'heriter du pont (`mtu=1` chez Proxmox), sans quoi
|
||||
il nait a 1500 quelle que soit la zone.
|
||||
|
||||
Quand les deux divergent, la panne est sournoise : la connexion s'etablit, les petites
|
||||
requetes passent, les grosses reponses restent suspendues. Elle depend alors entierement de
|
||||
la decouverte de MTU de chemin — donc de messages ICMP que n'importe quel pare-feu peut
|
||||
avaler en silence.
|
||||
|
||||
ET ELLE EST INVISIBLE TANT QUE LA FLOTTE TIENT SUR UN SEUL HYPERVISEUR : deux VM du meme
|
||||
noeud communiquent par le pont local, sans encapsulation. Mesure du 2026-08-10 : zones
|
||||
`t11`/`t17` a 1450, les 14 invites a 1500, et aucun symptome — les 14 etaient sur `asgard`.
|
||||
Le jour ou on repartit la flotte, tout casse d'un coup et rien ne dit pourquoi.
|
||||
|
||||
Le MTU attendu n'est pas saisi : il est lu dans `devis_sdn.py`, la source qui configure les
|
||||
zones. Le rattachement hote -> zone se derive du pont porte par l'inventaire (`t17serv` est
|
||||
un VNet de la zone `t17`).
|
||||
|
||||
Aucun acces reseau ici — c'est le playbook qui releve (D-23).
|
||||
|
||||
make mtu-mesurer
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def zones_attendues() -> dict[str, int]:
|
||||
"""MTU attendu par VNet, lu a la source qui configure les zones."""
|
||||
r = subprocess.run([sys.executable, str(RACINE / "scripts" / "devis_sdn.py"), "--json"],
|
||||
cwd=RACINE, capture_output=True, text=True)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit("Le devis SDN ne se genere pas :\n" + r.stderr.strip()[:300])
|
||||
par_vnet: dict[str, int] = {}
|
||||
for z in (json.loads(r.stdout).get("zones") or []):
|
||||
for v in (z.get("vnets") or []):
|
||||
par_vnet[str(v.get("vnet"))] = int(z.get("mtu") or 0)
|
||||
return par_vnet
|
||||
|
||||
|
||||
def analyser(releve: dict, attendus: dict[str, int]) -> tuple[list[str], list[str]]:
|
||||
ecarts: list[str] = []
|
||||
lignes: list[str] = []
|
||||
invites = releve.get("invites") or {}
|
||||
ponts = releve.get("ponts") or {}
|
||||
for hote in sorted(invites):
|
||||
vu = int(invites[hote] or 0)
|
||||
pont = str(ponts.get(hote) or "")
|
||||
attendu = attendus.get(pont)
|
||||
if attendu is None:
|
||||
# Hors SDN (pont classique) : le devis n'a pas d'attendu derive, il se tait
|
||||
# plutot que d'inventer un seuil.
|
||||
lignes.append(f" -- {hote:<16} {vu:>5} pont {pont or '(inconnu)'} — hors zone SDN")
|
||||
continue
|
||||
ok = vu == attendu
|
||||
lignes.append(f" {'ok ' if ok else 'ECART'} {hote:<16} {vu:>5} attendu {attendu} "
|
||||
f"(zone du pont {pont})")
|
||||
if not ok:
|
||||
ecarts.append(f"{hote} : carte a {vu}, zone {pont} a {attendu}")
|
||||
return ecarts, lignes
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--releve", default=None)
|
||||
a = ap.parse_args(argv)
|
||||
chemin = Path(a.releve) if a.releve else RACINE / "instance" / "devis-mtu.json"
|
||||
if not chemin.is_file():
|
||||
raise SystemExit(f"Aucun releve : {chemin}\nLancer d'abord `make mtu-mesurer`.")
|
||||
ecarts, lignes = analyser(json.loads(chemin.read_text()), zones_attendues())
|
||||
print("Devis du MTU — carte de l'invite contre MTU de sa zone\n")
|
||||
print("\n".join(lignes))
|
||||
if ecarts:
|
||||
print(f"\nECART : {len(ecarts)} invite(s) ne portent pas le MTU de leur zone.")
|
||||
for e in ecarts:
|
||||
print(" -", e)
|
||||
print("\n Corriger : `mtu: 1` sur la carte (herite du pont) — deja pose par\n"
|
||||
" `cloner_vm_debian.yml`. Une VM existante doit redemarrer pour le prendre.")
|
||||
return 1
|
||||
print("\nCONFORME : chaque invite porte le MTU de sa zone.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue