sonde plancher ; une exposition sans port n est plus attribuee a l edge
La zone publie l empreinte de ce que le plancher doit porter ; chaque machine la compare a son /etc/hosts. Au site, elle a revele que sauvegarde, pki et dns.genese.internal pointaient vers l edge (regression de mon deploiement du matin) : la derivation ne donne plus a l edge ce qui n a pas de port. Zone du site corrigee, SFTP des locataires verifie. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
ba2020d486
commit
eee6851c3b
13 changed files with 378 additions and 3 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -1,5 +1,56 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-09-29 (31) — Sonde `plancher` ; elle a trouvé une régression que j'avais causée au site, corrigée à la source
|
||||
|
||||
**La sonde `plancher` (rôle `client_sante`, sur toute machine qui rapporte).** Le plancher
|
||||
`/etc/hosts` et la zone PowerDNS suivent le même plan, chacun quand son rôle est rejoué
|
||||
(voir (30)). La zone publie désormais `_plancher.<domaine> TXT "n=… sha256=…"` : le nombre et
|
||||
l'empreinte des paires « nom adresse » que le plancher DOIT porter — flotte active ET
|
||||
planifiée, expositions du domaine. La sonde calcule la même chose sur `/etc/hosts`, en
|
||||
interrogeant l'autoritatif directement, sans transfert de zone (AXFR reste fermé). En cas
|
||||
d'écart elle nomme ce qu'elle peut : noms publiés absents (nombre), adresses différentes,
|
||||
noms inconnus de l'autoritatif. Manquant ou réadressé : critique ; en trop seulement :
|
||||
avertissement. `seulement_si: hosts_statiques_actif`. `test_empreinte_plancher.py` rend les
|
||||
deux gabarits sur un même inventaire et exige la même empreinte.
|
||||
|
||||
Chez les deux locataires : 26 machines sur 26 conformes (19 noms chacun). Mises en défaut
|
||||
sur `mon-01` (Technolibre), sur des copies du plancher : vide, sans `console` → critique,
|
||||
« 1 nom publié absent » ; `forge-01` en trop → avertissement nommé ; `mon-01` réadressé →
|
||||
critique, les deux adresses données.
|
||||
|
||||
**Ce qu'elle a trouvé au site — et ce que j'y avais cassé.** En (30), j'ai redéployé la zone
|
||||
du site sans simulation préalable, en attribuant la réécriture au seul `site-dnspub-01`. La
|
||||
zone faisait en réalité pointer `sauvegarde`, `pki` et `dns.genese.internal` vers l'edge du
|
||||
site (10.37.37.11), qui ne les sert pas. Les locataires sauvegardent vers
|
||||
`sauvegarde.genese.internal`, résolu par le DNS du site : de 05 h 23 à 11 h 51, ce nom menait
|
||||
à l'edge, où aucun SFTP n'attend. Aucune sauvegarde n'est tombée dans la fenêtre (elles
|
||||
tournent vers 02 h 30), mais celles de la nuit auraient toutes échoué.
|
||||
|
||||
**La cause est dans la dérivation.** `expositions_des_applications` attribuait tout FQDN
|
||||
d'un domaine à l'edge de ce domaine. Or un edge ne publie que ce qui a un port : nginx n'écrit
|
||||
un vhost que pour une exposition qui en porte un. Depuis que `genese.internal` a un edge
|
||||
(14 septembre), les trois expositions sans port lui étaient attribuées ; les anciens
|
||||
planchers du site, antérieurs, les envoyaient encore aux bonnes machines, et la zone chargée
|
||||
avant (30) aussi. **Corrigé** : une exposition sans port se sert elle-même
|
||||
(`test_expositions_sans_port.py`) ; la preuve de `prouver.py` qui refuse qu'une exposition
|
||||
retombe sur son propre groupe dans un écosystème à edge excepte désormais celles sans port.
|
||||
Zone du site redéployée après simulation (trois A, vers 10.37.35.11, .32.11, .34.11), cache
|
||||
d'Unbound vidé pour `genese.internal`. Depuis les deux locataires, `sauvegarde` rend
|
||||
10.37.35.11 et une session SFTP s'ouvre dans le bon dépôt. `site-dnspub-01`, dont le plancher
|
||||
avait été généré avec la dérivation fautive, rejoué seul : conforme.
|
||||
|
||||
**Correction de (30)** : la zone du site n'y avait pas seulement reçu `site-dnspub-01` —
|
||||
elle avait aussi envoyé ces trois noms vers l'edge.
|
||||
|
||||
**Ce qui reste, et qui est une décision.** Les 8 autres machines du site ont un plancher
|
||||
antérieur à l'edge : `forge`, `observatoire` et `vigie` y mènent directement aux services,
|
||||
et `console`, `site-dnspub-01` y manquent (le DNS du site les résout). La sonde les montre
|
||||
critiques. Les rejouer ferait passer le runner du site par l'edge pour joindre la forge en
|
||||
HTTPS : la lecture y a été éprouvée (`ls-remote` identique), mais l'edge limite une requête à
|
||||
16 Mo et `Set-OPS-public` en pèse 171 — un envoi de rattrapage n'y passerait pas. Et
|
||||
`client_pki` retiendra, à son prochain passage au site, `pki.genese.internal` sur
|
||||
`site-pki-01` plutôt que sur l'edge — le nom qu'il sert vraiment.
|
||||
|
||||
## 2026-09-29 (30) — `console` ne se résolvait pas : un plancher périmé et une zone jamais rechargée
|
||||
|
||||
**Constat.** Depuis l'edge de chaque locataire, `console.<domaine>` ne se résolvait pas,
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -317,6 +317,8 @@ test: ## Lance les tests unitaires (derivation de nomenclature et d'inventaire)
|
|||
python3 scripts/tests/test_fraicheur_icinga.py
|
||||
python3 scripts/tests/test_sondes_conditionnelles.py
|
||||
python3 scripts/tests/test_sondes_syntaxe.py
|
||||
python3 scripts/tests/test_empreinte_plancher.py
|
||||
python3 scripts/tests/test_expositions_sans_port.py
|
||||
|
||||
# LA SONDE QUI RAPPORTE CE QUI DISTINGUE (2026-08-28). Trois faux diagnostics en une
|
||||
# journee, tous dus a l'instrument : `curl` et `bash /dev/tcp` ecrasent « la machine
|
||||
|
|
|
|||
|
|
@ -69,3 +69,7 @@ client_sante_paquets:
|
|||
# machines ; ces seuils laissent donc voir une croissance longtemps avant la panne.
|
||||
client_sante_disque_avert: 80
|
||||
client_sante_disque_crit: 90
|
||||
|
||||
# Le plancher que la sonde « plancher » compare a l'autoritatif. Mise en defaut PAR
|
||||
# PARAMETRE : un fichier vide ou absent — la sonde doit alors voir tous les noms manquer.
|
||||
client_sante_plancher_fichier: "/etc/hosts"
|
||||
|
|
|
|||
|
|
@ -14,3 +14,13 @@ sondes:
|
|||
Reste-t-il de la place, et des inodes, sur chaque systeme de fichiers ? Seuls le
|
||||
cache apt et les depots de sauvegarde etaient surveilles : un disque plein arretait
|
||||
la base ou le courriel sans prevenir.
|
||||
- nom: plancher
|
||||
ttl: 5400
|
||||
# Posee sous la condition du plancher (voir `setops-sondes.conf.j2`, 2026-09-28).
|
||||
seulement_si: hosts_statiques_actif
|
||||
defini_par: hosts_statiques
|
||||
raison: >-
|
||||
Le plancher /etc/hosts de la machine dit-il la meme chose que l'autoritatif ? Les deux
|
||||
suivent le plan, chacun quand son role est rejoue. Le 2026-09-29, la console manquait
|
||||
aux planchers des deux locataires : depuis l'edge, elle ne se resolvait pas, et le
|
||||
plancher y est la seule resolution des noms internes.
|
||||
|
|
|
|||
|
|
@ -163,6 +163,18 @@
|
|||
group: root
|
||||
mode: "0750"
|
||||
|
||||
# LA SONDE « plancher » (2026-09-29), sous la condition meme du plancher : sans plancher
|
||||
# gere (`hosts_statiques_actif: false`), il n'y a rien a comparer. Posee ici pour la raison
|
||||
# de « disque » : ce role tourne sur tout hote qui rapporte, sans toucher au systeme.
|
||||
- name: Deposer la sonde du plancher de resolution
|
||||
ansible.builtin.template:
|
||||
src: sonde-plancher.sh.j2
|
||||
dest: /usr/local/lib/setops/sondes/plancher.sh
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0750"
|
||||
when: hosts_statiques_actif | default(true) | bool
|
||||
|
||||
- name: Relever les sondes reellement posees
|
||||
ansible.builtin.find:
|
||||
paths: "{{ client_sante_repertoire_sondes | default('/usr/local/lib/setops/sondes') }}"
|
||||
|
|
|
|||
96
roles/client_sante/templates/sonde-plancher.sh.j2
Normal file
96
roles/client_sante/templates/sonde-plancher.sh.j2
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/bin/bash
|
||||
# GENERE par Set-OPS (role client_sante). Ne pas editer a la main.
|
||||
#
|
||||
# SONDE « plancher » — le plancher /etc/hosts de cette machine dit-il la meme chose que
|
||||
# l'autoritatif du domaine ?
|
||||
#
|
||||
# POURQUOI (2026-09-29). Le plancher et la zone PowerDNS derivent du meme plan, mais
|
||||
# chacun ne le suit que quand son role est rejoue. La console a ete ajoutee au plan : la
|
||||
# zone l'a recue, les planchers non — sauf celui d'`ops-01`, redeploye depuis. Depuis
|
||||
# l'edge, `console` ne se resolvait pas, et rien ne le signalait. Sur ces machines, le
|
||||
# plancher est la SEULE resolution des noms internes : un nom qui y manque n'existe pas.
|
||||
#
|
||||
# COMMENT, SANS TRANSFERT DE ZONE. La zone publie `_plancher.<domaine> TXT "n=… sha256=…"`
|
||||
# : le nombre et l'empreinte des paires « nom adresse » que le plancher doit porter pour ce
|
||||
# domaine. On calcule la meme chose ici sur /etc/hosts. Si elles different, on nomme ce
|
||||
# qu'on peut : chaque nom du plancher est demande a l'autoritatif (en trop, readresse), et
|
||||
# l'ecart de nombre dit combien de noms publies manquent ici.
|
||||
#
|
||||
# L'autoritatif est interroge DIRECTEMENT : le resolveur qu'utilisent ces machines peut
|
||||
# ignorer ce domaine (c'est le cas chez les locataires, qui passent par celui du site).
|
||||
#
|
||||
# Mise en defaut PAR PARAMETRE : `client_sante_plancher_fichier` (un plancher vide).
|
||||
set -uo pipefail
|
||||
|
||||
DOMAINE='{{ domaine_interne | default('') }}'
|
||||
AUTORITE='{{ hostvars[groups['serveur_powerdns'][0]].ansible_host | default('') if (groups.get('serveur_powerdns') or []) else '' }}'
|
||||
FICHIER='{{ client_sante_plancher_fichier }}'
|
||||
|
||||
if [[ -z "${DOMAINE}" || -z "${AUTORITE}" ]]; then
|
||||
echo "Pas d'autoritatif interne dans cet ecosysteme : rien a quoi comparer le plancher."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
txt=$(dig +short +time=3 +tries=2 TXT "_plancher.${DOMAINE}" "@${AUTORITE}" 2>/dev/null | tr -d '"')
|
||||
if [[ "${txt}" != n=*" sha256="* ]]; then
|
||||
echo "L'autoritatif (${AUTORITE}) ne publie pas l'empreinte du plancher (_plancher.${DOMAINE} : ${txt:-rien}) — zone anterieure, ou injoignable : comparaison impossible."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Les paires « nom adresse » du plancher pour CE domaine : toute ligne, tout nom qui finit
|
||||
# par `.<domaine>` (le nom court des hotes, en alias, n'est pas compte ; la zone non plus).
|
||||
local_=$(python3 - "${FICHIER}" "${DOMAINE}" <<'PY'
|
||||
import hashlib, sys
|
||||
fichier, domaine = sys.argv[1], sys.argv[2]
|
||||
paires = set()
|
||||
try:
|
||||
lignes = open(fichier, encoding="utf-8").read().splitlines()
|
||||
except OSError:
|
||||
lignes = []
|
||||
for l in lignes:
|
||||
champs = l.split("#", 1)[0].split()
|
||||
if len(champs) < 2:
|
||||
continue
|
||||
for nom in champs[1:]:
|
||||
if nom.endswith("." + domaine):
|
||||
paires.add(f"{nom} {champs[0]}")
|
||||
tri = sorted(paires)
|
||||
print(f"n={len(tri)} sha256={hashlib.sha256(chr(10).join(tri).encode()).hexdigest()}")
|
||||
for p in tri:
|
||||
print(p)
|
||||
PY
|
||||
)
|
||||
empreinte=$(printf '%s\n' "${local_}" | head -1)
|
||||
n_pub=${txt#n=}; n_pub=${n_pub%% *}
|
||||
n_loc=${empreinte#n=}; n_loc=${n_loc%% *}
|
||||
perf="| noms=${n_loc};;;0; publies=${n_pub};;;0;"
|
||||
|
||||
if [[ "${empreinte}" == "${txt}" ]]; then
|
||||
echo "Plancher conforme a l'autoritatif : ${n_loc} nom(s) de ${DOMAINE}, memes adresses. ${perf}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ILS DIFFERENT : on nomme ce qu'on peut. Chaque nom du plancher est demande a l'autoritatif.
|
||||
# Un nom qu'il ignore est en trop ici (ou une machine planifiee, que la zone ne publie pas
|
||||
# encore) ; une autre adresse est un readressage que l'un des deux n'a pas suivi.
|
||||
en_trop=(); readresses=(); connus=0; i=0
|
||||
while read -r nom ip; do
|
||||
(( i++ >= 40 )) && break
|
||||
pub=$(dig +short +time=2 +tries=1 A "${nom}" "@${AUTORITE}" 2>/dev/null | tail -1)
|
||||
if [[ -z "${pub}" ]]; then en_trop+=("${nom%%.*}")
|
||||
elif [[ "${pub}" != "${ip}" ]]; then readresses+=("${nom%%.*}=${ip}/${pub}"); connus=$((connus + 1))
|
||||
else connus=$((connus + 1)); fi
|
||||
done < <(printf '%s\n' "${local_}" | tail -n +2)
|
||||
manquants=$(( n_pub - connus ))
|
||||
|
||||
msg="Le plancher de cette machine et l'autoritatif divergent (${n_loc} nom(s) ici, ${n_pub} publie(s))"
|
||||
[[ ${manquants} -gt 0 ]] && msg+=" ; ${manquants} nom(s) publie(s) absent(s) d'ici"
|
||||
[[ -n "${readresses[*]}" ]] && msg+=" ; autre adresse : ${readresses[*]}"
|
||||
[[ -n "${en_trop[*]}" ]] && msg+=" ; inconnu(s) de l'autoritatif : ${en_trop[*]}"
|
||||
msg+=". Rejouer hosts_statiques (ou serveur_powerdns, si c'est la zone qui date)."
|
||||
# Un nom qui MANQUE ou une adresse qui differe casse une resolution : critique. Des noms en
|
||||
# trop seulement (une machine retiree du plan) ne cassent rien : avertissement.
|
||||
if (( manquants > 0 )) || [[ -n "${readresses[*]}" ]]; then
|
||||
echo "${msg} ${perf}"; exit 2
|
||||
fi
|
||||
echo "${msg} ${perf}"; exit 1
|
||||
|
|
@ -52,3 +52,32 @@ $TTL {{ serveur_powerdns_ttl }}
|
|||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
{# L'EMPREINTE DU PLANCHER (2026-09-29). Le plancher `/etc/hosts` de chaque machine et
|
||||
cette zone derivent du meme plan, mais pas au meme moment : chacun suit le plan quand son
|
||||
role est rejoue. Le 2026-09-29, la console manquait aux planchers des deux locataires
|
||||
pendant que la zone la portait. Pour que chaque machine puisse comparer SANS transfert de
|
||||
zone (AXFR reste ferme), la zone publie ce que le plancher DOIT contenir pour ce domaine :
|
||||
le nombre de paires « nom adresse » et leur empreinte sha256.
|
||||
|
||||
LE MEME ENSEMBLE QUE `hosts_statiques/templates/hosts.j2`, pas celui de cette zone : la
|
||||
flotte ACTIVE ET PLANIFIEE (le plancher resout un clone avant qu'il soit actif ; la zone
|
||||
non), plus les expositions de ce domaine. `test_empreinte_plancher.py` rend les deux
|
||||
gabarits sur un meme inventaire et exige la meme empreinte : si l'un change seul, le
|
||||
test le dit. La sonde `plancher` (role `client_sante`) calcule le cote machine. #}
|
||||
{% set _plancher = [] %}
|
||||
{% for h in (groups.get('hotes_actifs', []) + groups.get('hotes_planifies', [])) | unique | sort %}
|
||||
{% if hostvars[h].ansible_host is defined %}
|
||||
{% set _ = _plancher.append(h ~ '.' ~ serveur_powerdns_zone ~ ' ' ~ hostvars[h].ansible_host) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if serveur_powerdns_publier_expositions | default(false) %}
|
||||
{% for expo in serveur_powerdns_expositions | default([]) if expo.domaine == serveur_powerdns_zone %}
|
||||
{% set edge_hosts = groups.get(expo.edge, []) %}
|
||||
{% if edge_hosts and hostvars[edge_hosts[0]].ansible_host is defined %}
|
||||
{% set _ = _plancher.append(expo.fqdn ~ ' ' ~ hostvars[edge_hosts[0]].ansible_host) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% set _paires = _plancher | unique | sort %}
|
||||
_plancher IN TXT "n={{ _paires | length }} sha256={{ _paires | join('\n') | hash('sha256') }}"
|
||||
|
|
|
|||
|
|
@ -568,7 +568,16 @@ def expositions_des_applications(applications: dict, domaines: dict, edge: str |
|
|||
# ecrivait un plancher SANS alias. Le certificat de la forge portait
|
||||
# `forge.genese.internal`, et ce nom ne resolvait nulle part — du TLS correct
|
||||
# sur un nom que personne ne pouvait appeler.
|
||||
edge_dom = conf.get("edge") or app.get("groupe")
|
||||
#
|
||||
# SANS PORT, PAS D'EDGE (2026-09-29). Un edge ne publie que ce qui a un port :
|
||||
# nginx n'ecrit un vhost que pour une exposition qui en porte un. Le site expose
|
||||
# aussi `sauvegarde` (SFTP), `pki` et `dns`, sans port web. Depuis que
|
||||
# `genese.internal` a un edge, ces trois noms lui etaient attribues : la zone et le
|
||||
# plancher les faisaient pointer vers `site-edge-01`, qui ne les sert pas. Le
|
||||
# 2026-09-29, `sauvegarde.genese.internal` repondait l'edge, et les sauvegardes des
|
||||
# deux locataires y seraient allees frapper un sshd qui n'est pas le leur.
|
||||
# Une exposition sans port se sert donc elle-meme, comme avant l'edge.
|
||||
edge_dom = (conf.get("edge") if app.get("port") else None) or app.get("groupe")
|
||||
if edge is not None and edge_dom != edge:
|
||||
continue
|
||||
# SANS DOMAINE PUBLIC DECLARE, LE DOMAINE EST CELUI QUE PORTE LE FQDN
|
||||
|
|
|
|||
|
|
@ -4013,7 +4013,9 @@ def _repli_edge(nom: str, applications: dict, domaines: dict, a_un_edge: bool) -
|
|||
return [f"edge : {nom} — `{e['fqdn']}` retombe sur son propre groupe (`{e['edge']}`) "
|
||||
f"alors que l'ecosysteme a un edge : ni vhost ni SAN ne le retiendront"
|
||||
for e in expositions_des_applications(applications, domaines)
|
||||
if e["edge"] != "serveur_nginx" and str(e["edge"]) not in ecrits]
|
||||
# Sans port, aucun vhost ne peut la servir : elle se sert elle-meme, a juste
|
||||
# titre (`sauvegarde`, `pki`, `dns` au site — voir `expositions_des_applications`).
|
||||
if e.get("port") and e["edge"] != "serveur_nginx" and str(e["edge"]) not in ecrits]
|
||||
|
||||
|
||||
def _repli_hotes_du_site(enfants: dict, hv: dict) -> list[str]:
|
||||
|
|
|
|||
115
scripts/tests/test_empreinte_plancher.py
Normal file
115
scripts/tests/test_empreinte_plancher.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""L'empreinte que la zone publie est celle que la sonde calcule sur le plancher rendu.
|
||||
|
||||
POURQUOI (2026-09-29). La zone PowerDNS publie `_plancher.<domaine> TXT "n=… sha256=…"` :
|
||||
ce que le plancher `/etc/hosts` DOIT porter pour ce domaine. La sonde `plancher` calcule la
|
||||
meme chose sur /etc/hosts. Les deux calculs vivent dans deux gabarits de deux roles
|
||||
(`serveur_powerdns/templates/zone.db.j2`, `hosts_statiques/templates/hosts.j2`) et dans le
|
||||
script de la sonde (`client_sante/templates/sonde-plancher.sh.j2`) : une liste qui en suit
|
||||
une autre prend du retard. Si l'un change seul, chaque machine crierait « divergent » sur
|
||||
une flotte saine.
|
||||
|
||||
On rend donc les deux gabarits sur UN MEME inventaire fictif — hotes actifs ET planifies
|
||||
(le plancher resout un clone avant la zone), expositions de deux domaines, un hote sans
|
||||
adresse — et on execute le code Python de la sonde sur le plancher rendu. Les empreintes
|
||||
doivent etre identiques. Controle negatif : un nom retire du plancher doit la changer.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
import jinja2
|
||||
|
||||
ROLES = Path(__file__).resolve().parents[2] / "roles"
|
||||
DOMAINE = "exemple.internal"
|
||||
|
||||
|
||||
def environnement() -> jinja2.Environment:
|
||||
env = jinja2.Environment(undefined=jinja2.ChainableUndefined, extensions=["jinja2.ext.do"])
|
||||
env.filters["hash"] = lambda v, algo="sha1": hashlib.new(algo, str(v).encode()).hexdigest()
|
||||
env.filters["bool"] = bool
|
||||
return env
|
||||
|
||||
|
||||
def contexte() -> dict:
|
||||
hostvars = {
|
||||
"mon-01": {"ansible_host": "10.9.20.21"},
|
||||
"edge-01": {"ansible_host": "10.9.16.11"},
|
||||
"dns-01": {"ansible_host": "10.9.19.11"},
|
||||
"neuf-01": {"ansible_host": "10.9.21.99"}, # planifie : au plancher, pas en A
|
||||
"sans-ip": {},
|
||||
}
|
||||
expositions = [
|
||||
{"fqdn": f"console.{DOMAINE}", "domaine": DOMAINE, "edge": "serveur_nginx"},
|
||||
{"fqdn": f"vigie.{DOMAINE}", "domaine": DOMAINE, "edge": "serveur_nginx"},
|
||||
{"fqdn": "www.exemple.ca", "domaine": "exemple.ca", "edge": "serveur_nginx"},
|
||||
]
|
||||
return {
|
||||
"groups": {"hotes_actifs": ["mon-01", "edge-01", "dns-01", "sans-ip"],
|
||||
"hotes_planifies": ["neuf-01"], "serveur_nginx": ["edge-01"],
|
||||
"serveur_powerdns": ["dns-01"]},
|
||||
"hostvars": hostvars,
|
||||
# plancher
|
||||
"hosts_statiques_domaine": DOMAINE, "hosts_statiques_publier_expositions": True,
|
||||
"hosts_statiques_expositions": expositions, "hosts_statiques_externes": [],
|
||||
# zone
|
||||
"serveur_powerdns_zone": DOMAINE, "serveur_powerdns_publier_expositions": True,
|
||||
"serveur_powerdns_expositions": expositions, "serveur_powerdns_records": [],
|
||||
"serveur_powerdns_inventory_groups": ["hotes_actifs"], "serveur_powerdns_nameserver": "ns1",
|
||||
}
|
||||
|
||||
|
||||
def code_de_la_sonde() -> str:
|
||||
gabarit = (ROLES / "client_sante/templates/sonde-plancher.sh.j2").read_text(encoding="utf-8")
|
||||
m = re.search(r"<<'PY'\n(.*?)\nPY\n", gabarit, re.S)
|
||||
assert m, "bloc Python introuvable dans sonde-plancher.sh.j2"
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def empreinte_sonde(plancher: str) -> str:
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".hosts", delete=False) as f:
|
||||
f.write(plancher)
|
||||
r = subprocess.run([sys.executable, "-", f.name, DOMAINE], input=code_de_la_sonde(),
|
||||
capture_output=True, text=True)
|
||||
Path(f.name).unlink()
|
||||
return r.stdout.splitlines()[0]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
env, ctx = environnement(), contexte()
|
||||
plancher = env.from_string((ROLES / "hosts_statiques/templates/hosts.j2")
|
||||
.read_text(encoding="utf-8")).render(**ctx)
|
||||
zone = env.from_string((ROLES / "serveur_powerdns/templates/zone.db.j2")
|
||||
.read_text(encoding="utf-8")).render(**ctx)
|
||||
m = re.search(r'^_plancher IN TXT "([^"]+)"', zone, re.M)
|
||||
if not m:
|
||||
print("ECHEC la zone ne publie pas `_plancher IN TXT`.")
|
||||
return 1
|
||||
publie, calcule = m.group(1), empreinte_sonde(plancher)
|
||||
echecs = 0
|
||||
if publie != calcule:
|
||||
print(f"ECHEC la zone publie « {publie} », la sonde calcule « {calcule} » sur le plancher "
|
||||
f"rendu du MEME inventaire : les deux gabarits ne comptent plus la meme chose.")
|
||||
echecs += 1
|
||||
if not publie.startswith("n=6 "):
|
||||
print(f"ECHEC attendu 6 paires (4 hotes avec adresse dont 1 planifie, 2 expositions du "
|
||||
f"domaine) ; publie : {publie}")
|
||||
echecs += 1
|
||||
ampute = "\n".join(l for l in plancher.splitlines() if "console." not in l)
|
||||
if empreinte_sonde(ampute) == publie:
|
||||
print("ECHEC controle negatif : un plancher sans `console` rend la meme empreinte.")
|
||||
echecs += 1
|
||||
if echecs:
|
||||
return 1
|
||||
print(f"OK — zone et plancher rendus sur le meme inventaire : meme empreinte ({publie[:18]}…) ; "
|
||||
f"un nom retire la change.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
43
scripts/tests/test_expositions_sans_port.py
Normal file
43
scripts/tests/test_expositions_sans_port.py
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Une exposition SANS PORT n'est jamais attribuee a l'edge du domaine.
|
||||
|
||||
POURQUOI (2026-09-29). Un edge ne publie que ce qui a un port : nginx n'ecrit un vhost que
|
||||
pour une exposition qui en porte un. Le site expose aussi `sauvegarde` (SFTP), `pki` et
|
||||
`dns` sans port web. Quand `genese.internal` a recu un edge, la derivation lui a attribue
|
||||
ces trois noms ; la zone et le plancher les ont fait pointer vers `site-edge-01`, et les
|
||||
sauvegardes des deux locataires y seraient allees frapper un sshd qui n'est pas le leur.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
from inventory_rules import expositions_des_applications # noqa: E402
|
||||
|
||||
APPS = {"applications": {
|
||||
"forge": {"groupe": "serveur_forgejo", "hote": "forge-01", "port": 443, "expose": ["forge.ex.internal"]},
|
||||
"backup": {"groupe": "serveur_backup_site", "hote": "backup-01", "expose": ["sauvegarde.ex.internal"]},
|
||||
"pki": {"groupe": "serveur_step_ca", "hote": "pki-01", "port": None, "expose": ["pki.ex.internal"]},
|
||||
}}
|
||||
DOMS = {"domaines_publics": {"ex.internal": {"edge": "serveur_nginx"}}}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
edges = {e["fqdn"]: e["edge"] for e in expositions_des_applications(APPS, DOMS)}
|
||||
attendu = {"forge.ex.internal": "serveur_nginx",
|
||||
"sauvegarde.ex.internal": "serveur_backup_site",
|
||||
"pki.ex.internal": "serveur_step_ca"}
|
||||
if edges != attendu:
|
||||
print(f"ECHEC attendu {attendu}, obtenu {edges}")
|
||||
return 1
|
||||
sans_edge = {e["fqdn"]: e["edge"] for e in expositions_des_applications(APPS, {"domaines_publics": {}})}
|
||||
if sans_edge["forge.ex.internal"] != "serveur_forgejo":
|
||||
print(f"ECHEC sans edge declare, le service doit se servir lui-meme : {sans_edge}")
|
||||
return 1
|
||||
print("OK — avec port : l'edge du domaine ; sans port : le service lui-meme.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
graph LR
|
||||
R["<b>sante</b>"]
|
||||
R -.->|"sonde « disque »"| ICINGA[["Icinga"]]
|
||||
R -.->|"sonde « plancher »"| ICINGA[["Icinga"]]
|
||||
```
|
||||
|
||||
## Qui lui parle
|
||||
|
|
@ -20,6 +21,7 @@ graph LR
|
|||
| sonde | TTL | ce qu'elle voit |
|
||||
|---|---|---|
|
||||
| `disque` | 5400 s | Reste-t-il de la place, et des inodes, sur chaque systeme de fichiers ? Seuls le cache apt et les depots de sauvegarde etaient surveilles : un disque plein arretait la base ou le courriel sans prevenir. |
|
||||
| `plancher` | 5400 s | Le plancher /etc/hosts de la machine dit-il la meme chose que l'autoritatif ? Les deux suivent le plan, chacun quand son role est rejoue. Le 2026-09-29, la console manquait aux planchers des deux locataires : depuis l'edge, elle ne se resolvait pas, et le plancher y est la seule resolution des noms internes. |
|
||||
|
||||
## Ce qu'il expose en séries
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Chaque fiche est construite depuis ce que le rôle **déclare lui-même** — `m
|
|||
| [`client_metrique`](Rôle-client_metrique) | 1 | 1 | — |
|
||||
| [`client_pki`](Rôle-client_pki) | — | 1 | — |
|
||||
| [`client_resolveur`](Rôle-client_resolveur) | — | — | — |
|
||||
| [`client_sante`](Rôle-client_sante) | — | 1 | — |
|
||||
| [`client_sante`](Rôle-client_sante) | — | 2 | — |
|
||||
| [`client_smtp`](Rôle-client_smtp) | — | — | — |
|
||||
| [`cloud_init`](Rôle-cloud_init) | — | — | — |
|
||||
| [`cloud_init_retrait`](Rôle-cloud_init_retrait) | — | — | — |
|
||||
|
|
|
|||
Loading…
Reference in a new issue