materiel : la frontiere expose ses temperatures, seul le Prometheus du site les lit

os-node_exporter sur la patte de supervision, declare dans la carte du boitier : cible et
hote Icinga en derivent. Les flux vers la frontiere ne s'ouvrent plus a tous pour un role
qui n'est pas le socle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-09-17 10:20:46 -04:00
parent 49ceb81a4d
commit ee386dca00
7 changed files with 87 additions and 12 deletions

View file

@ -1,5 +1,26 @@
# CHANGELOG — Set-OPS # CHANGELOG — Set-OPS
## 2026-09-17 (2) — La frontiere rejoint la supervision du materiel
- **Exportateur** : greffon officiel `os-node_exporter` installe et configure par l'API
d'OPNsense, n'ecoutant que sur la patte de supervision (`10.37.36.1:9100`), collecteurs
utiles seulement. Declare dans `opnsense.yml` (`opnsense_exportateur_metriques`) : c'est ce
fait du boitier qui fait deriver la cible Prometheus (`job="frontiere"`, `hote="bifrost-1"`)
et l'hote Icinga — sans lui, rien n'est derive.
- **Flux** `serveur_prometheus -> frontiere:9100`. Le bloc des flux vers la frontiere ouvrait
TOUT flux a chaque locataire et chaque zone : juste pour l'heure du socle, beaucoup trop
large pour une scrutation. Un role autre que le socle ne part desormais que de SES machines
du site. Et le saut des flux `frontiere` chez les locataires arrive AVANT la creation de
l'alias du role : le plan montrait deux alias `SETOPS_*_SRV_PROMETHEUS` que rien ne
referencait.
- Plan de la frontiere : 1 regle (opt9, tcp/9100, `SETOPS_SITE_SERVEUR_PROMETHEUS ->
SETOPS_FRONTIERE`), appliquee et chargee.
- **Carte d'orientation** : les equipements ne sont plus « sans supervision » (hyperviseurs et
frontiere) ; les commutateurs le restent.
Mesure : `up{job="frontiere"}` = 1 ; `check_materiel.py --hote bifrost-1` : OK, 4 coeurs a
41 °C. `make prouver` : CONFORME, 82 OK.
## 2026-09-17 (1) — Le materiel se regarde et se juge : hyperviseurs ## 2026-09-17 (1) — Le materiel se regarde et se juge : hyperviseurs
Les hyperviseurs exposaient deja 68 temperatures, leurs ventilateurs, SMART et l'usure NVMe, Les hyperviseurs exposaient deja 68 temperatures, leurs ventilateurs, SMART et l'usure NVMe,

View file

@ -21,7 +21,7 @@
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | | 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), 1 nom(s) surveille(s) sans reference orpheline. | | P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 1 nom(s) surveille(s) sans reference orpheline. |
| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. | | P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 42 groupes classes, aucun cycle, aucune arete en arriere ; playbooks/site.yml a jour. |
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 flux, schéma + matrice OK. | | P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 120 flux, schéma + matrice OK. |
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | | 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 | | 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. | | P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
@ -46,7 +46,7 @@
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 71 scripts expliques et atteignables, 131 cibles make documentees, 69 roles avec README. | | P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 71 scripts expliques et atteignables, 131 cibles make documentees, 69 roles avec README. |
| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (147 cle(s) declaree(s) par l'instance). | | P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 37 exigence(s) de role, toutes satisfaites (147 cle(s) declaree(s) par l'instance). |
| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 42 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). | | P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 42 revendication(s) de port, aucune collision entre roles co-localises (35 groupes). |
| P34 | Chaque document declare son lecteur | — | ✅ OK | 48 document(s) declarent leur lecteur (42 genere(s) exempte(s)). | | P34 | Chaque document declare son lecteur | — | ✅ OK | 48 document(s) declarent leur lecteur (43 genere(s) exempte(s)). |
| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). | | P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 4 application(s) exigeant une base l'ont toutes (3 entree(s) au registre). |
| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 8 hote(s) de l'ecosysteme et 3 du site detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | | P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 8 hote(s) de l'ecosysteme et 3 du site 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. | | P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
@ -60,9 +60,9 @@
| 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). | | 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 | | 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). | | 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 97 chemin(s) qui existent, et ses 7 chiffres correspondent a la mesure. | | P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 99 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 (137 lignes). | | 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 (138 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 282 regles `pass`), tous non consignes et tous motives. | | 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 283 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 | | 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. | | 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 — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b | | P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |

View file

@ -26,7 +26,7 @@ README de rôles). Cette page comble ces deux trous.
| rôles | 69 | `roles/*/` | | rôles | 69 | `roles/*/` |
| README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette | | README de rôles | 69 | `roles/*/README.md` — l'écart avec la ligne au-dessus est la dette |
| documents | 44 | `docs/*.md` | | documents | 44 | `docs/*.md` |
| pièces d'audit | 47 | `docs/audit/*` | | pièces d'audit | 48 | `docs/audit/*` |
| unités de wiki | 27 | `wiki/*.md` | | unités de wiki | 27 | `wiki/*.md` |
| décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` | | décisions en vigueur | 85 | lignes `\| **D-nn** \|` de `decisions-architecture.md` |
| décisions renversées | 3 | lignes `\| **D-nn** —` du même document | | décisions renversées | 3 | lignes `\| **D-nn** —` du même document |
@ -72,7 +72,7 @@ Ce que je re-découvre sinon. **Consulter avant de concevoir un nouveau mécanis
| Voûte au déploiement | secret jamais en clair ; **une voûte, une clé** depuis le 2026-08-28 | `ANSIBLE_VAULT_IDENTITY_LIST` construit par `scripts/voutes.py` (clé nommée `~/.config/setops-vault-<dépôt>`) ; déréférencé par `lookup('vars', <nom>)` | `autorisation.md` §6.1 | | Voûte au déploiement | secret jamais en clair ; **une voûte, une clé** depuis le 2026-08-28 | `ANSIBLE_VAULT_IDENTITY_LIST` construit par `scripts/voutes.py` (clé nommée `~/.config/setops-vault-<dépôt>`) ; déréférencé par `lookup('vars', <nom>)` | `autorisation.md` §6.1 |
| Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` | | Multi-instance | un dépôt par écosystème ; l'active = symlink `instance/`, les autres **découvertes par convention** (dossiers frères, aucun registre) | active : symlink `instance/` ; découverte : `scripts/instances.py` / `devis_reseau.py` (glob `../*/plan/nomenclature.yml` avec `index`) ; garde-fou collision : preuve **P21** | `multi-instances.md` |
| Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 | | Exposition → edge | app expose un FQDN public servi par un edge | `plan/domaines.yml` + `expose` (applications) | `bindings-conception.md` §4 |
| Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* — hyperviseurs, commutateurs, frontière — n'ont toujours ni inventaire, ni sauvegarde de configuration, ni supervision | `hebergeur-exploitation.md` | | Exploitation de l'hébergeur | ses **opérations** n'appartiennent à aucun tenant et restent **hors overlay** | **à moitié construit** : les *VM* du site ont leur inventaire (`scripts/site_inventaire.py`), leur socle, leur durcissement, leurs sauvegardes et leur supervision (`site-mon-01`, 2026-09-02). Les *équipements* : hyperviseurs et frontière **supervisés** depuis le 2026-09-17 — températures, ventilateurs, SMART, usure NVMe, un catalogue (`scripts/materiel.py`) lu par le tableau Grafana « Matériel » et le service Icinga `materiel` (`supervision-conception.md`). Les commutateurs restent sans supervision ; aucun équipement n'a de sauvegarde de configuration | `hebergeur-exploitation.md` |
| Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` | | Authentification | web → Keycloak ; LDAP source unique ; secours par `sudo`, formulaire local non annoncé | `<rôle>_connexion_locale: false` (grafana, forgejo, nextcloud) ; garde de version Forgejo ≥ 10 | `authentification.md` |
| Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) | | Accès & habilitations | Set-OPS **amorce** un accès sysadmin puis se retire ; les appartenances aux groupes ne sont **jamais réconciliées** — c'est une personne qui gouverne | **construit et éprouvé** (2026-08-08) : rôle `amorcage_acces` (idempotence par existence, D-67), groupes projetés en rôles par `serveur_keycloak`, `meta/acces.yml` dans les 5 rôles web ; chaîne LDAP → Keycloak → groupe → service exercée de bout en bout sur Icinga Web 2 | `autorisation.md` (§6 = runbook de reprise) |
| **DNS public** | le locataire **écrit** ses zones `autorite: primaire-cache`, le site les **sert** en secondaire, un site pair les réplique. Transfert ouvert **à la clé TSIG seule** (`allow-axfr-ips` et TSIG sont alternatifs dans PowerDNS) ; instance `pdns@public` à part pour que le site ne puisse jamais interroger la zone `.internal` | `roles/serveur_dns_public/`, `roles/serveur_powerdns/tasks/zones-publiques.yml` ; relations dérivées dans `scripts/site_inventaire.py` ; mots de flux `dns_public_site` / `primaires_dns_locataires` ; garde **P82** | `dns-interne.md` §Zones publiques | | **DNS public** | le locataire **écrit** ses zones `autorite: primaire-cache`, le site les **sert** en secondaire, un site pair les réplique. Transfert ouvert **à la clé TSIG seule** (`allow-axfr-ips` et TSIG sont alternatifs dans PowerDNS) ; instance `pdns@public` à part pour que le site ne puisse jamais interroger la zone `.internal` | `roles/serveur_dns_public/`, `roles/serveur_powerdns/tasks/zones-publiques.yml` ; relations dérivées dans `scripts/site_inventaire.py` ; mots de flux `dns_public_site` / `primaires_dns_locataires` ; garde **P82** | `dns-interne.md` §Zones publiques |

View file

@ -111,6 +111,7 @@
| `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). | | `serveur_powerdns` | egress | 53 | tcp | externe | clair | Repli TCP de la résolution sortante du serveur autoritatif (réponses dépassant la taille UDP). |
| `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). | | `serveur_prometheus` | ingress | 9090 | tcp | localhost | clair | Console Prometheus consommée en local par Grafana co-localisé (pas d'exposition inter-nœud). |
| `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. | | `serveur_prometheus` | egress | 9100 | tcp | client_metrique | tls | Scrape des node_exporter (HTTPS via cert step-ca) sur chaque nœud instrumenté. |
| `serveur_prometheus` | egress | 9100 | tcp | frontiere | clair | Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni charge dans la supervision. |
| `serveur_prometheus` | egress | 9100 | tcp | fabric | clair | Scrape des hyperviseurs : la machine qui PORTE les VM etait invisible de la supervision qui les surveille. Elle ne peut pas pousser (route par defaut gelee, D-57), donc on la tire. | | `serveur_prometheus` | egress | 9100 | tcp | fabric | clair | Scrape des hyperviseurs : la machine qui PORTE les VM etait invisible de la supervision qui les surveille. Elle ne peut pas pousser (route par defaut gelee, D-57), donc on la tire. |
| `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. | | `serveur_redis` | ingress | 6379 | tcp | localhost | clair | Cache/verrous consommés uniquement par l'application co-localisée (ex. Nextcloud). Aucune exposition inter-nœud. |
| `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. | | `serveur_resolveur` | ingress | 53 | udp | flotte | clair | Toute la flotte du tenant résout ici — et nulle part ailleurs. |
@ -128,7 +129,7 @@
## Synthèse chiffrement ## Synthèse chiffrement
- **clair** : 49 flux - **clair** : 50 flux
- **n-a** : 8 flux - **n-a** : 8 flux
- **ssh** : 8 flux - **ssh** : 8 flux
- **starttls** : 6 flux - **starttls** : 6 flux

View file

@ -24,6 +24,17 @@ flux:
# Prometheus tire donc, dans le sens que la frontiere route deja pour `serveur_ops_site`. # Prometheus tire donc, dans le sens que la frontiere route deja pour `serveur_ops_site`.
# 7887 lignes de metriques sur `asgard`, dont 1005 unites systemd avec leur etat — soit # 7887 lignes de metriques sur `asgard`, dont 1005 unites systemd avec leur etat — soit
# davantage que ce qu'un ping ou un porteur de sante auraient dit. # davantage que ce qu'un ping ou un porteur de sante auraient dit.
# LA FRONTIERE ELLE-MEME (2026-09-17) : ses temperatures, sa charge, ses interfaces. Le
# greffon `os-node_exporter` n'ecoute que sur sa patte de supervision.
- sens: egress
port: 9100
protocole: tcp
pair: frontiere
chiffrement: clair
raison: >-
Scrape de l'exportateur de la frontiere (os-node_exporter), sur sa seule patte de
supervision : l'equipement qui voit passer TOUT le trafic n'avait ni temperature ni
charge dans la supervision.
- sens: egress - sens: egress
port: 9100 port: 9100
protocole: tcp protocole: tcp

View file

@ -535,6 +535,12 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
tenants_sans_inventaire.append(nom) tenants_sans_inventaire.append(nom)
continue continue
for role, fl in retenus: for role, fl in retenus:
# AVANT L'ALIAS (2026-09-17). Le saut plus bas arrivait APRES la creation de
# l'alias du role : un flux vers la frontiere declare par `serveur_prometheus`
# posait `SETOPS_<tenant>_SRV_PROMETHEUS` chez chaque locataire, qu'aucune regle
# ne referencait. Le plan l'a montre avant toute ecriture.
if "frontiere" in _pairs(fl):
continue
if role == ROLE_SOCLE: if role == ROLE_SOCLE:
destination = alias_tenant destination = alias_tenant
else: else:
@ -1339,7 +1345,7 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
alias["SETOPS_FRONTIERE"] = { alias["SETOPS_FRONTIERE"] = {
"type": "host", "type": "host",
"contenu": _pattes_frontiere, "contenu": _pattes_frontiere,
"description": "Pattes de la frontiere active (autorite de temps)", "description": "Pattes de la frontiere active (heure, metriques)",
} }
# LES PATTES D'ARRIVEE. Un locataire entre par le lien de transit — ses # LES PATTES D'ARRIVEE. Un locataire entre par le lien de transit — ses
# passerelles de zone sont tenues par le SDN, pas par la frontiere. Une # passerelles de zone sont tenues par le SDN, pas par la frontiere. Une
@ -1350,7 +1356,23 @@ def construire(tenants: list[tuple[str, str, dict]]) -> dict:
_portees_f += [("SETOPS_SITE", z) for z in _portees_f += [("SETOPS_SITE", z) for z in
sorted({_if_de(m.get("reseau")) for m in _machines_site})] sorted({_if_de(m.get("reseau")) for m in _machines_site})]
for _role_f, _fl_f in _flux_frontiere: for _role_f, _fl_f in _flux_frontiere:
for _src_f, _if_f in _portees_f: # LE SOCLE PARLE A LA FRONTIERE DEPUIS PARTOUT ; UN ROLE, DEPUIS SES MACHINES
# (2026-09-17). L'heure est demandee par toutes les machines : les portees
# ci-dessus sont justes pour elle. La scrutation des metriques de la
# frontiere, elle, n'est l'affaire que du Prometheus du SITE — l'ouvrir a
# chaque locataire et a chaque zone aurait donne la carte de charge du
# boitier a qui la demande.
if _role_f == "serveur_debian":
_portees_role = _portees_f
else:
_portees_role = [(nom_alias(_role_f, "SITE"), z) for z in
sorted({_if_de(m.get("reseau")) for m in _machines_site
if _role_f in _services_site.get(m["nom"], [])})]
if not _portees_role:
print(f"note : {_role_f} declare un flux vers la frontiere et aucune "
f"machine du site ne le porte — aucune regle emise.",
file=sys.stderr)
for _src_f, _if_f in _portees_role:
regles.append({ regles.append({
"sens": "in", "sens": "in",
"interface": _if_f, "interface": _if_f,

View file

@ -536,6 +536,21 @@ def inventaire() -> dict:
if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11.")} if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11.")}
_fabric_metriques = sorted(_fabric_nommes) _fabric_metriques = sorted(_fabric_nommes)
# LA FRONTIERE, QUAND SON EXPORTATEUR EST DECLARE (2026-09-17). L'adresse est celle de sa
# patte sur la zone declaree, lue dans l'underlay ; le nom est celui du boitier.
_frontiere_metriques: dict[str, str] = {}
try:
_carte_o = U.chemin().resolve().parent / "opnsense.yml" if U.chemin() else None
_expo_f = ((yaml.safe_load(_carte_o.read_text(encoding="utf-8")) or {})
.get("opnsense_exportateur_metriques") or {}) if _carte_o and _carte_o.is_file() else {}
except (OSError, yaml.YAMLError):
_expo_f = {}
if _expo_f:
for _h in U.hotes(u):
if (str(_h.get("role")) == "frontiere" and str(_h.get("etat") or "actif") == "actif"
and str(_h.get("reseau")) == str(_expo_f.get("patte")) and _h.get("ip")):
_frontiere_metriques[f"{_h['ip']}:{_expo_f.get('port', 9100)}"] = str(_h.get("nom"))
# CE QUE LA SUPERVISION IRA VOIR : chaque patte de la frontiere, nommee par la zone # CE QUE LA SUPERVISION IRA VOIR : chaque patte de la frontiere, nommee par la zone
# qu'elle sert. Les commutateurs restent dehors — D-48 les tient hors flotte, et rien # qu'elle sert. Les commutateurs restent dehors — D-48 les tient hors flotte, et rien
# ne les rend interrogeables sans leur ouvrir un acces qu'on ne veut pas leur ouvrir. # ne les rend interrogeables sans leur ouvrir un acces qu'on ne veut pas leur ouvrir.
@ -827,7 +842,10 @@ def inventaire() -> dict:
"setops_materiel_hotes": [ "setops_materiel_hotes": [
{"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "fabric", {"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "fabric",
"role": "hyperviseur"} "role": "hyperviseur"}
for _c, _n in sorted(_fabric_nommes.items(), key=lambda x: x[1])]} for _c, _n in sorted(_fabric_nommes.items(), key=lambda x: x[1])]
+ [{"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "frontiere",
"role": "frontiere"}
for _c, _n in sorted(_frontiere_metriques.items(), key=lambda x: x[1])]}
if (_fabric_nommes and ({"serveur_grafana", "serveur_icinga"} if (_fabric_nommes and ({"serveur_grafana", "serveur_icinga"}
& set(_groupes_de(nom)))) else {}), & set(_groupes_de(nom)))) else {}),
# La fabric, en job separe — voir `_fabric_metriques` plus haut. # La fabric, en job separe — voir `_fabric_metriques` plus haut.
@ -835,7 +853,9 @@ def inventaire() -> dict:
# `hotes` : chaque serie porte `hote="asgard"`. Sans lui, un tableau du # `hotes` : chaque serie porte `hote="asgard"`. Sans lui, un tableau du
# materiel legende « 192.168.11.43:9100 » — et c'est a l'exploitant de se # materiel legende « 192.168.11.43:9100 » — et c'est a l'exploitant de se
# souvenir quelle machine chauffe. # souvenir quelle machine chauffe.
[{"job": "fabric", "cibles": _fabric_metriques, "hotes": _fabric_nommes}]} [{"job": "fabric", "cibles": _fabric_metriques, "hotes": _fabric_nommes}]
+ ([{"job": "frontiere", "cibles": sorted(_frontiere_metriques),
"hotes": _frontiere_metriques}] if _frontiere_metriques else [])}
if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}), if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}),
**communes, **communes,
# En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le # En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le