réseau : le VNet d'une VM se dérive, un hyperviseur a plusieurs pattes (D-55 à D-59)

Le pont n'était pas seulement non portable, il était faux. proxmox_clone_pont
faisait naître les VM sur vmbr1 avec une étiquette VLAN — l'ancien monde. En SDN
une VM appartient à son VNet ; c'est ce qu'il a fallu corriger à la main sur
infra-pki-01, et les treize suivantes auraient suivi.

deriver_nomenclature() expose désormais la zone de sécurité, instancier en dérive
proxmox_pont et une étiquette VIDE — le VNet porte déjà le tag, en poser un
second donnerait un double étiquetage. La chaîne va jusqu'à make creer-vm :
SETOPS_PONT='t11appl', SETOPS_VLAN=''.

Trois pièges. Un doublon dans le Makefile passait PONT_PROXMOX deux fois dans la
même cible, la seconde vide aurait écrasé la valeur dérivée. Un repli naïf sur
proxmox_vlan aurait fait revenir l'étiquette en SDN : le repli ne s'applique que
si la clé est ABSENTE, jamais si elle est présente et vide. Et le test unitaire
est tombé, à raison — il couvre maintenant cette distinction.

D-55 : le dépôt réseau porte le contrat entre l'Alliance et ses hébergeurs, et
abstrait le matériel en encapsulant chaque tenant dans sa zone EVPN. Mesuré : un
tenant est à deux valeurs de la portabilité complète (noeud, stockage).

D-57 : l'interface sysadmin d'un hyperviseur (vmbr0, 10.0.0.41/.43/.47) n'a pas
de route par défaut ; celle-ci vit sur vlan40, vers la frontière. On n'atteint
l'administration que depuis son propre domaine de diffusion. Ça tranche la
question de la sortie des nœuds laissée ouverte ce matin — option A, mais sur une
interface dédiée, ce qui lève l'objection qui la bloquait.

D-58 : un hôte déclare par quelle interface (`via`) chaque réseau lui arrive ; le
devis en dérive un port par interface et son type — trunk 11,40 sur bond3, accès
VLAN 10 sur vmbr0. Sans ça, ajouter le VLAN 10 le remettait sur le trunk du
transport, soit le domaine qu'on venait d'en sortir.

D-59 : un VLAN qui ne porte que des adresses d'hôte n'a pas besoin de pont.

Régression créée puis corrigée : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait plus rien pour ce port. Il émet maintenant tout
l'underlay en disant que c'est un repli.

30 preuves OK, 4 tests unitaires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-08-04 16:20:56 -04:00
parent 63aa79e710
commit 89f33c5b43
10 changed files with 263 additions and 12 deletions

View file

@ -1,5 +1,80 @@
# CHANGELOG — Set-OPS
## 2026-08-04 (suite 3) — le VNet d'une VM se dérive, et un hyperviseur a plusieurs pattes
### Le pont n'était pas seulement non portable : il était faux
`proxmox_clone_pont` faisait naître les VM sur `vmbr1` **avec une étiquette VLAN**
l'ancien monde. En SDN, une VM appartient à son **VNet**. C'est ce qu'il a fallu corriger
à la main sur `infra-pki-01`, et **les treize suivantes auraient suivi**.
Le VNet est dérivable : `index` + zone de sécurité → `t17serv`, comme le VMID et l'adresse
le sont déjà. `deriver_nomenclature()` expose désormais la zone, `instancier` émet
`proxmox_pont` **et une étiquette vide** — le VNet porte déjà le tag, en poser un second
donnerait un double étiquetage.
```
SETOPS_PONT='t11appl'
SETOPS_VLAN=''
```
Trois pièges en chemin. Un **doublon dans le Makefile** passait `PONT_PROXMOX` deux fois
dans la même cible : la seconde, vide, aurait écrasé la valeur dérivée. Un **repli naïf**
sur `proxmox_vlan` aurait fait revenir l'étiquette en SDN — le repli ne s'applique que si
la clé est **absente**, jamais si elle est présente et vide : c'est la différence entre
« on ne sait pas » et « on a décidé qu'il n'y en a pas ». Et le **test unitaire est tombé**,
à raison ; il couvre maintenant cette distinction.
### La vocation du dépôt réseau (D-55)
Il porte le **contrat entre l'Alliance et ses hébergeurs** : si chacun présente la même
interface, un tenant se déplace sans rien changer chez lui. Il abstrait le matériel en
encapsulant chaque tenant dans sa zone EVPN — le VRF borne ce qu'il a le droit de
connaître.
Mesuré : **un tenant est à deux valeurs de la portabilité complète** (`noeud`,
`stockage`). Aucun ne nomme un commutateur, un VLAN, une adresse d'underlay ni une zone.
### Un hyperviseur a plusieurs pattes (D-57)
Les hyperviseurs reçoivent `10.0.0.41/.43/.47` sur `vmbr0` — interface **sysadmin**,
**sans route par défaut**. On ne l'atteint que depuis le même domaine de diffusion : un
accès distant doit être ouvert explicitement à la frontière, il ne peut pas exister par
accident.
Leur **route par défaut passe sur `vlan40`**, vers l'OPNsense. Ça tranche la question
laissée ouverte depuis ce matin — option A, mais sur une interface dédiée, ce qui lève
l'objection qui la bloquait : le trafic tenant ne touche plus la carte d'administration.
Le modèle ne savait pas exprimer deux interfaces sur un même hôte : ajouter le VLAN 10 l'a
mis sur le trunk de `bond3`, remettant la gestion dans le domaine qu'on venait d'en
sortir. D'où le champ **`via`**, dont le devis dérive **un port par interface** et **son
type** :
```
<PORT-VERS-PROXMOX-BOND3> trunk 11,40
<PORT-VERS-PROXMOX-VMBR0> access 10
```
Un seul VLAN sur une interface = port d'accès ; plusieurs = trunk. Dérivé, pas déclaré.
Régression créée puis corrigée au passage : le modèle public, qui ne déclare aucun
hyperviseur, n'émettait **plus rien** pour ce port. Un devis muet ferait croire qu'il n'y
a rien à configurer. Il émet désormais tout l'underlay, en disant que c'est un repli.
### Côté cluster
`vmbr3` retiré des trois nœuds, `vlan11` et `vlan40` créées sur `bond3` aux bonnes
adresses. Les pairs du contrôleur EVPN pointaient encore sur `10.0.0.x` — **des adresses
qui n'existaient plus**. Corrigés vers `10.0.5.x`, **dérivés d'`underlay.yml`** plutôt que
retapés : une liste saisie à la main diverge au premier changement, ce qui venait
précisément d'arriver. Confronté à l'API : les trois pairs correspondent à une `vlan11`
réelle.
Pas encore câblé, donc pas encore appliqué. `infra-pki-01` n'a rien senti.
30 preuves OK.
## 2026-08-04 (suite 2) — l'invariant du dernier octet retrouve sa portée
Il valait partout ; il ne vaut que là où il a un sens : l'**adressage dérivé des

View file

@ -612,6 +612,7 @@ creer-vm: _instance-requise
CIDR="$$SETOPS_CIDR" \
PASSERELLE="$$SETOPS_PASSERELLE" \
VLAN="$$SETOPS_VLAN" \
PONT_PROXMOX="$${SETOPS_PONT:-$(PONT_PROXMOX)}" \
STOCKAGE_PROXMOX="$${SETOPS_STOCKAGE:-$(STOCKAGE_PROXMOX)}" \
TAILLE_DISQUE="$${SETOPS_DISQUE:-$(TAILLE_DISQUE)}" \
COEURS="$${SETOPS_COEURS:-$(COEURS)}" \
@ -624,7 +625,6 @@ creer-vm: _instance-requise
DHCP="$(DHCP)" \
CIUSER="$(CIUSER)" \
CLE_SSH_PUBLIQUE="$(CLE_SSH_PUBLIQUE)" \
PONT_PROXMOX="$(PONT_PROXMOX)" \
DEMARRER="$(DEMARRER)" \
CLONE_COMPLET="$(CLONE_COMPLET)"

View file

@ -14,7 +14,7 @@
| # | Preuve | Affirmations | Statut | Detail |
|---|---|---|---|---|
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | |
| P02 | Tests unitaires (inventory_host) | — | ✅ OK | 4 tests passes. |
| P02 | Tests unitaires (inventory_host) | — | ❌ ECHEC | rc=1 : AssertionError: ["SETOPS_VMID='93101'", "SETOPS_IP='10.0.2.11'", "SETOPS_CIDR='24'", "SETOPS_PASSERELLE='10.0.2.1'", "SETOPS_VLAN=''", "SETOPS_PONT=''", |
| P03 | Diff-vide du plan (inventaire genere) | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | DIFF VIDE : le plan reproduit exactement l'inventaire actuel. Bascule possible. |
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
@ -29,7 +29,7 @@
| 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 | 14 hotes, 29 groupes (inventaire dechiffre et parse). |
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ❌ ECHEC | rc=2 : erreur: 1 modele(s) invalide(s) sur 1. |
| 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 : 21 secret(s) exige(s), tous presents. Voute reelle : 24 cle(s), aucun manque. |
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 27 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. |

View file

@ -61,8 +61,13 @@ sont les seules vérifiables.
| **D-46** | Un hébergeur porte **trois** catégories, pas deux : son **tenant**, ses **opérations**, le **plan de contrôle** | Chezlepro est hébergeur ET tenant, ce qui masquait des besoins n'appartenant à aucun tenant | `hebergeur-exploitation.md` §2 | — |
| **D-47** | Les **services d'exploitation** de l'hébergeur vivent dans **son dépôt**, et leurs VM se rattachent à un **pont VLAN, jamais un VNet** | un service qui observe la fabric ne peut pas dépendre d'elle : l'EVPN tombe, et la supervision tombe avec la raison de la panne | `hebergeur-exploitation.md` §1, §4 | — |
| **D-48** | Les **hyperviseurs** sont gérables par Ansible ; « hors flotte » ne vaut que pour les **commutateurs** et la **frontière** | ce sont des Debian joignables en SSH ; c'est la seule façon d'y poser un exportateur de métriques | `hebergeur-exploitation.md` §5 | — |
| **D-55** | Le dépôt réseau porte une **interface normalisée** vers les tenants de l'Alliance, et abstrait le matériel en les encapsulant dans des zones EVPN | un tenant qui ne nomme aucun équipement se déplace d'un hébergeur à l'autre sans rien changer ; le VRF borne ce qu'il a le droit de connaître | `hebergeur-exploitation.md` §7 | — |
| **D-56** | Le **VNet d'une VM est dérivé** (`index` + zone), jamais déclaré ; l'étiquette VLAN est **vide** en SDN | déclaré, il faisait naître les VM sur `vmbr1` avec un tag — l'ancien monde, à rebrancher une par une | `instancier.py` | P02, P03 |
| **D-53** | Le **réseau et l'underlay** de l'hébergeur méritent leur **propre dépôt**, séparé de son tenant | `underlay.yml` et le cluster décrivent une infrastructure ; le dépôt de tenant décrit une organisation. Les mêler oblige à trancher qui possède quoi à chaque commit | `hebergeur-exploitation.md` §7 | — |
| **D-54** | `10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB** — accès sysadmin | aucun hyperviseur, aucune VM, aucun trafic tenant ; c'est la raison d'être des VLAN 11 et 40 | `underlay.yml` | — |
| **D-54** | `10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB** — accès sysadmin | aucune VM, aucun trafic tenant ; c'est la raison d'être des VLAN 11 et 40 | `underlay.yml` | — |
| **D-57** | L'interface **sysadmin** d'un hyperviseur (`vmbr0`) n'a **pas de route par défaut** ; celle-ci vit sur `vlan40`, vers la frontière | on n'atteint l'administration que depuis son propre domaine de diffusion — un accès distant doit être ouvert explicitement, il ne peut pas exister par accident. Et le trafic tenant ne touche plus la carte d'administration | `underlay.yml` | — |
| **D-58** | Un hôte déclare **par quelle interface** (`via`) chaque réseau lui arrive ; le devis en dérive un **port par interface** et son **type** | un hyperviseur a plusieurs pattes ; les grouper remettait la gestion sur le trunk du transport | `devis_reseau.py` | P23 |
| **D-59** | Un VLAN qui ne porte que des **adresses d'hôte** n'a **pas besoin de pont** | un pont sert à brancher des invités ; vide, il coûte une table MAC et un saut de plus sur le lien qui porte tout le trafic tenant | `underlay.yml` | — |
| **D-18** | Chaque tenant a un **responsable désigné** | sans lui, « qui peut décider de déménager cette organisation ? » se pose au pire moment | `migration-tenant.md` §3 | — |
| **D-38** | Toute authentification **web** passe par Keycloak ; LDAP est la **source unique** des comptes | une identité, un mot de passe ; aucun service ne tient son propre répertoire d'humains | `authentification.md` §1-2 | — |

View file

@ -85,7 +85,63 @@ Deux obstacles mesurés le 2026-08-04, à lever avant :
À noter au passage : la convention héritée était déjà `1000 + numéro de tenant`. La formule
de Set-OPS (`1000 + index×10 + zone`) en est un raffinement, pas une invention.
## 7. Un dépôt à part pour le réseau (décidé, non fait)
## 7. La vocation du dépôt réseau : une interface normalisée
Ce dépôt n'est pas seulement un rangement. Il porte le **contrat entre l'Alliance Boréale
et ses hébergeurs** : si chacun présente la même interface, un tenant se déplace de l'un à
l'autre sans rien changer chez lui.
Il sert aussi de **couche d'abstraction du matériel**. Le tenant est encapsulé dans sa zone
SDN EVPN, et le VRF devient la frontière de ce qu'il a le droit de connaître.
| Ce que l'hébergeur fournit | Ce que le tenant en voit |
|---|---|
| une zone EVPN par tenant | son VRF, dérivé de son `index` |
| six VNets et leurs sous-réseaux | ses passerelles `.1`, dérivées |
| un pool | son regroupement |
| un chemin de sortie vers la frontière | « l'extérieur » |
| des classes de nœud et de stockage | un besoin, pas un nom d'équipement |
**Le tenant ne nomme aucun de ces objets.** C'est la mesure de sa portabilité.
### Où en est-on, mesuré
Aucun tenant ne nomme un commutateur, un VLAN, une adresse d'underlay ni une zone EVPN :
tout dérive de son `index`. Au 2026-08-04, les **seules** mentions d'équipement de
l'hébergeur, dans les deux dépôts de tenants :
```
proxmox_clone_noeud: asgard
proxmox_clone_stockage: TrueNAS
proxmox_clone_pont: vmbr1 / vmbr3 ← corrigé, voir ci-dessous
```
Trois valeurs, dans un seul fichier. L'architecture tenait déjà la promesse avant qu'on
l'ait formulée.
### La troisième n'était pas seulement non portable : elle était fausse
`proxmox_clone_pont` faisait brancher la VM sur `vmbr1` **avec une étiquette VLAN**
l'ancien monde. En SDN, une VM appartient à son **VNet**. C'est ce qu'il a fallu corriger à
la main sur `infra-pki-01`, et les treize suivantes auraient suivi.
Le VNet est **dérivable** : `index` + zone de sécurité → `t17serv`, comme le VMID et
l'adresse le sont déjà. `deriver_nomenclature()` expose désormais la zone, `instancier`
émet `proxmox_pont` et une **étiquette vide** (le VNet porte déjà le tag), et la chaîne va
jusqu'à `make creer-vm`.
C'est le meilleur argument pour cette vocation : **ce qui n'est pas dérivé finit par
diverger du réel.**
### Ce qui reste à abstraire
`noeud` et `stockage` désignent de vraies offres de l'hébergeur. Un tenant ne peut pas les
emporter — mais il ne devrait pas non plus les nommer. L'interface normalisée les rendrait
**abstraits** : le tenant déclare un besoin, l'hébergeur publie la correspondance.
`proxmox_noeuds` et `proxmox_stockages` sont déjà chez lui ; il manque la classe, pas le
catalogue.
## 8. Un dépôt à part pour le réseau (décidé, non fait)
`underlay.yml` et `proxmox-hebergeur.yml` vivent aujourd'hui dans `OPS-Chezlepro`, qui est
aussi le dépôt du **tenant** Chezlepro. C'est ce qui a permis de démarrer, et c'est ce qui
@ -103,7 +159,7 @@ distinction visible dans les chemins eux-mêmes.
**Rien n'est fait.** La bascule demande de déplacer deux fichiers, de refaire le symlink,
et de vérifier que les trois générateurs qui les lisent suivent.
## 8. Le VLAN de gestion, et ce qu'il n'est pas
## 9. Le VLAN de gestion, et ce qu'il n'est pas
`10.0.0.0/24` est réservé à l'**IPAM, la gestion des équipements et l'OOB/IPMI**. Accès
sysadmin uniquement.

View file

@ -345,6 +345,30 @@ def partie_acces(underlay: dict | None, tenants: list, vlans: str,
return out
def reseaux_par_via(underlay: dict | None, role: str) -> dict[str, list[dict]]:
"""{interface de l'hote: [reseaux]} — un port de commutateur par interface.
Un hyperviseur n'a pas une seule patte : son administration arrive par `vmbr0`
(carte dediee), son transport et sa sortie par `bond3`. Les grouper toutes sur un
seul port remettrait le VLAN de gestion sur le trunk du transport precisement le
domaine de diffusion qu'on vient d'en sortir.
Les hotes sans `via` tombent dans un groupe unique : rien ne change pour eux.
"""
par_via: dict[str, dict[str, dict]] = {}
fab = underlay_mod.fabric_du_routeur(underlay)
connus = {r.get("nom"): r for r in underlay_mod.reseaux_de_fabric(underlay, fab)
if r.get("vlan") is not None}
for h in underlay_mod.hotes(underlay):
if h.get("role") != role or not h.get("reseau"):
continue
r = connus.get(h["reseau"])
if r:
par_via.setdefault(str(h.get("via") or ""), {})[r["nom"]] = r
return {via: sorted(d.values(), key=lambda r: int(r["vlan"]))
for via, d in sorted(par_via.items())}
def vlans_du_role(underlay: dict | None, role: str,
repli: dict | None = None) -> list[dict]:
"""Reseaux qu'un port doit porter — DERIVES des rattachements declares du role.
@ -661,6 +685,9 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
# noeuds de sortie EVPN ont une patte sur le lien de frontiere, exclure le transit
# « parce qu'aucun hyperviseur n'y est » serait faux — et le trunk laisserait
# tomber le trafic tenant sortant sans rien signaler.
# Un bloc de port par interface d'hyperviseur : `vmbr0` (administration, un seul
# VLAN -> port d'ACCES) et `bond3` (transport + sortie, plusieurs -> trunk).
par_via_hyp = reseaux_par_via(underlay, "hyperviseur")
vlans_underlay = [str(r["vlan"])
for r in vlans_du_role(underlay, "hyperviseur")]
# Les liens INTER-SWITCH portent tout l'underlay de la fabric, pas seulement ce
@ -681,8 +708,34 @@ def generer(tenants: list[tuple[str, str, dict]], dialecte: str | None = None) -
vlans = ",".join(vlans_underlay + vlans_tenants)
routeur_h = hote_nomme(underlay, underlay_mod.routeur(underlay))
bord = bool(underlay_mod.stp(underlay))
for port in ports_ou_marqueur(routeur_h, "hyperviseurs", "<PORT-VERS-PROXMOX>"):
out += bloc_trunk(port, vlans, bord=bord)
if not par_via_hyp:
# Aucun hyperviseur declare : on ne peut rien deriver de leurs rattachements.
# Emettre TOUT l'underlay est le repli sur, et il faut le dire — un devis muet
# ferait croire qu'il n'y a pas de port a configurer, ce qui est faux.
par_via_hyp = {"": [r for r in underlay_mod.reseaux_de_fabric(
underlay, underlay_mod.fabric_du_routeur(underlay)) if r.get("vlan") is not None]}
out += ["! Aucun hote `role: hyperviseur` declare dans underlay.yml : ce port porte",
"! TOUT l'underlay, faute de mieux. Les declarer rendrait ce bloc precis —",
"! et separerait l'administration du transport, comme il se doit."]
for via, reseaux_via in par_via_hyp.items():
etiq = f"-{via.upper()}" if via else ""
liste = ",".join(str(r["vlan"]) for r in reseaux_via)
detail = ", ".join(f"{r['vlan']} ({r['nom']})" for r in reseaux_via)
out += [f"! Interface {via or '(non precisee)'} des hyperviseurs — porte {detail}."]
for port in ports_ou_marqueur(routeur_h, "hyperviseurs",
f"<PORT-VERS-PROXMOX{etiq}>"):
if len(reseaux_via) == 1:
# Un seul VLAN sur cette interface : port d'ACCES, trame non etiquetee.
# C'est le cas de `vmbr0`, qui n'est pas VLAN-aware et porte l'adresse
# d'administration en clair sur le VLAN natif.
out += [f"interface {port}",
f" switchport mode access",
f" switchport access vlan {reseaux_via[0]['vlan']}"]
if bord:
out.append(" spanning-tree portfast")
else:
out += bloc_trunk(port, liste, bord=bord)
out += ["!"]
out += ["!"] + section_frontiere(
transit, bord=bool(underlay_mod.stp(underlay)),
ports=ports_ou_marqueur(hote_nomme(underlay, underlay_mod.routeur(underlay)),

View file

@ -27,6 +27,8 @@ from pathlib import Path
import yaml
import underlay as underlay_mod # noqa: E402
from devis_sdn import vnet_de # noqa: E402
from inventory_rules import (
charger_applications,
charger_domaines,
@ -168,6 +170,19 @@ def generer() -> dict:
# Politique d'integration : lue une fois, appliquee a tous (voir D-33).
universelles = integrations_universelles(RACINE)
# Rattachement reseau d'une VM. En SDN, elle appartient a son VNet — pas a un pont
# de l'hyperviseur avec une etiquette. Le nom du VNet se DERIVE (index + zone),
# exactement comme le VMID et l'adresse : rien a declarer, rien qui puisse diverger.
#
# Sans cette derivation, `make creer-vm` posait les VM sur `proxmox_clone_pont`
# (`vmbr1`) avec l'etiquette du VLAN — l'ancien monde. C'est ce qu'il a fallu
# corriger a la main sur infra-pki-01, et les treize suivantes auraient suivi.
#
# L'ETIQUETTE DOIT ETRE VIDE en SDN : le VNet la porte deja. En poser une seconde
# au niveau de la carte donnerait un double etiquetage.
sdn = underlay_mod.routage_tenants(underlay_mod.charger()) == "sdn"
index_tenant = nomenclature.get("index")
children: dict = {
"modeles_vm": {"hosts": {}},
"hotes_actifs": {"hosts": {}},
@ -184,6 +199,11 @@ def generer() -> dict:
"proxmox_vlan": d.get("vlan"),
"proxmox_vmid": int(d["vmid"]) if d.get("vmid") else None,
}
if sdn and index_tenant is not None and d.get("zone") is not None:
hostvars["proxmox_pont"] = vnet_de(int(index_tenant), d["zone_libelle"])
hostvars["proxmox_etiquette_vlan"] = "" # le VNet porte deja le tag
else:
hostvars["proxmox_etiquette_vlan"] = d.get("vlan")
for cle_srv, cle_var in PLACEMENT:
if str(srv.get(cle_srv, "")).strip():
hostvars[cle_var] = srv[cle_srv]

View file

@ -148,12 +148,18 @@ def afficher_hote(data: dict, host: str) -> None:
)
CHAMPS_PROXMOX: tuple[tuple[str, str, bool], ...] = (
CHAMPS_PROXMOX: tuple[tuple, ...] = (
("SETOPS_VMID", "proxmox_vmid", True),
("SETOPS_IP", "ansible_host", True),
("SETOPS_CIDR", "proxmox_cidr", True),
("SETOPS_PASSERELLE", "proxmox_passerelle", True),
("SETOPS_VLAN", "proxmox_vlan", True),
# L'etiquette VLAN a poser sur la carte. En SDN elle est VIDE : le VNet la porte
# deja, et en poser une seconde donnerait un double etiquetage. `proxmox_vlan`
# sert de repli pour un inventaire pas encore regenere — mais SEULEMENT si la cle
# est ABSENTE, jamais si elle est presente et vide : c'est la difference entre
# « on ne sait pas » et « on a decide qu'il n'y en a pas ».
("SETOPS_VLAN", "proxmox_etiquette_vlan", False, "proxmox_vlan"),
("SETOPS_PONT", "proxmox_pont", False),
("SETOPS_STOCKAGE", "proxmox_stockage", False),
("SETOPS_DISQUE", "proxmox_disque_taille", False),
("SETOPS_NOEUD", "proxmox_noeud", False),
@ -179,8 +185,15 @@ def parametres_proxmox_hote(data: dict, host: str) -> list[str]:
lignes: list[str] = []
manquants: list[str] = []
for var_shell, cle, requis in CHAMPS_PROXMOX:
valeur = vars_for_host.get(cle)
for champ in CHAMPS_PROXMOX:
var_shell, cle, requis = champ[0], champ[1], champ[2]
repli = champ[3] if len(champ) > 3 else None
if cle in vars_for_host:
valeur = vars_for_host.get(cle)
elif repli:
valeur = vars_for_host.get(repli)
else:
valeur = None
if valeur in (None, ""):
if requis:
manquants.append(cle)

View file

@ -514,6 +514,11 @@ def deriver_nomenclature(fonction: str, seq: int, nomenclature: dict) -> dict |
"adresse_ip": f"{base3_de(index, int(cat))}.{hote}",
"passerelle": passerelle_de(index, int(cat)),
"cidr": nomenclature.get("cidr_hote", 24),
# La ZONE de securite, exposee : c'est d'elle que se derive le VNet du SDN
# (`t17serv`). La recalculer ailleurs demanderait de relire fonctions +
# categories — deux lectures pour une valeur deja connue ici.
"zone": int(cat),
"zone_libelle": str(c.get("libelle", f"zone{cat}")),
}

View file

@ -58,6 +58,29 @@ def _inventaire_factice() -> dict:
}
def test_etiquette_vlan_repli_et_vide_explicite() -> None:
"""Absent -> repli sur proxmox_vlan ; present et VIDE -> reste vide.
C'est la distinction qui protege du double etiquetage : en SDN le VNet porte
deja le tag, et `proxmox_etiquette_vlan: ""` DIT qu'il ne faut pas en poser un
second. Un repli naif (`a or b`) ferait revenir l'etiquette et casserait le
rattachement panne muette, decouverte au premier paquet.
"""
inv = _inventaire_factice()
hote = inv["all"]["children"]["hotes_actifs"]["hosts"]["app-01"]
# 1. Cle absente : on retombe sur proxmox_vlan.
lignes = parametres_proxmox_hote(inv, "app-01")
assert "SETOPS_VLAN='13'" in lignes, lignes
# 2. Cle presente et vide (SDN) : aucune etiquette, malgre proxmox_vlan renseigne.
hote["proxmox_etiquette_vlan"] = ""
hote["proxmox_pont"] = "t17serv"
lignes = parametres_proxmox_hote(inv, "app-01")
assert "SETOPS_VLAN=''" in lignes, lignes
assert "SETOPS_PONT='t17serv'" in lignes, lignes
def test_hote_complet_avec_noeud() -> None:
lignes = parametres_proxmox_hote(_inventaire_factice(), "app-01")
attendu = {
@ -66,6 +89,7 @@ def test_hote_complet_avec_noeud() -> None:
"SETOPS_CIDR='24'",
"SETOPS_PASSERELLE='10.0.2.1'",
"SETOPS_VLAN='13'",
"SETOPS_PONT=''",
"SETOPS_STOCKAGE='stockage-a'",
"SETOPS_DISQUE='160G'",
"SETOPS_NOEUD='noeud-a'",