materiel : un catalogue de capteurs, un tableau Grafana, un verdict Icinga
Les hyperviseurs exposaient temperatures, ventilateurs, SMART et usure NVMe sans que rien les regarde. scripts/materiel.py porte capteurs et seuils ; Grafana en tire le tableau Materiel, Icinga un service materiel par hyperviseur. Premier verdict : le disque sda de gandalf (OSD Ceph) a 56 secteurs en attente, stables. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8ac9f53f05
commit
49ceb81a4d
12 changed files with 895 additions and 4 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
|
@ -1,5 +1,29 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 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,
|
||||
collectes par Prometheus depuis le 2026-09-10 — et regardes par personne.
|
||||
|
||||
- **`scripts/materiel.py`** : un catalogue de capteurs et de seuils, lu par Grafana ET Icinga.
|
||||
Releve sur les trois cartes ASUS TUF X670E / Ryzen 9 7900X : `AUXTIN*` non cables, `PCH_*` a 0,
|
||||
DDR5 et cartes reseau exposees seulement par le noyau 6.14 de vishnu, SSD Samsung qui portent
|
||||
leur temperature dans l'attribut 190.
|
||||
- **Prometheus** : chaque cible de la fabric porte `hote="asgard"`.
|
||||
- **Grafana** : tableau « Set-OPS — Materiel » (27 panneaux) genere depuis le catalogue.
|
||||
- **Icinga** : un hote par hyperviseur, juge par `up` ; service `materiel` (`check_materiel.py`).
|
||||
|
||||
TROUVE EN LE CONSTRUISANT : le disque `sda` de gandalf (WD Red Plus 10 To, OSD HDD `osd.4` de
|
||||
Ceph, 26 225 h) porte 56 secteurs en attente, 17 irreparables et 2 realloues — stables sur une
|
||||
semaine, maximum a vie 63 °C. Ceph `HEALTH_OK`. Avertissement dans Icinga ; a remplacer.
|
||||
|
||||
Deux fautes eprouvees avant d'etre laissees : jointures refusees apres le reetiquetage
|
||||
(doublons de series — cote droit desormais agrege), et un 422 rapporte comme « Prometheus
|
||||
injoignable » (la sonde dit maintenant ce que Prometheus refuse).
|
||||
|
||||
Mesure : asgard OK (6 capteurs), vishnu OK (13), gandalf AVERTISSEMENT (secteurs sda). Toutes
|
||||
les requetes du tableau repondent. `make prouver` : CONFORME, 82 OK.
|
||||
|
||||
## 2026-09-16 (10) — Publier un service du site sur l'Internet : la redirection devient une capacite
|
||||
|
||||
Phase 2, quatrieme volet. Le devis du site sautait tout flux entrant `externe` (« rien d'autre
|
||||
|
|
|
|||
118
docs/audit/preuve-2026-09-17.md
Normal file
118
docs/audit/preuve-2026-09-17.md
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
# Preuve de conformite — Set-OPS — 2026-09-17
|
||||
|
||||
> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer
|
||||
> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ;
|
||||
> aucune validation n'est reimplementee ici. Voir le mode d'emploi :
|
||||
> [`docs/audit/README.md`](README.md), et le registre trace :
|
||||
> [`docs/audit/affirmations.md`](affirmations.md).
|
||||
|
||||
- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml`
|
||||
- **Verdict** : ✅ CONFORME (82 OK · 0 echec · 0 saute)
|
||||
|
||||
## Preuves
|
||||
|
||||
| # | Preuve | Affirmations | Statut | Detail |
|
||||
|---|---|---|---|---|
|
||||
| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK | } \| to_nice_json }}`. |
|
||||
| P02 | Tests unitaires (inventaire, raser, ecritures du plan, rendu du GUI) | — | ✅ OK | OK |
|
||||
| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 5 instance(s) verifiee(s) — instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0 : plan et inventaire applique coincident. |
|
||||
| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | |
|
||||
| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | |
|
||||
| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. |
|
||||
| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check), 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. |
|
||||
| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 40 rôles, 119 flux, schéma + matrice OK. |
|
||||
| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). |
|
||||
| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | serveur_resolveur_site |
|
||||
| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. |
|
||||
| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. |
|
||||
| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. |
|
||||
| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. |
|
||||
| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 13 hotes, 33 groupes (inventaire dechiffre et parse). |
|
||||
| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. |
|
||||
| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 33 secret(s) exige(s), tous presents. (Voute reelle non lisible ici : verification sautee.) |
|
||||
| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 30 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. |
|
||||
| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. |
|
||||
| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 6 instance(s) federee(s), aucun index en collision. |
|
||||
| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (22 sections). |
|
||||
| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 14 reseau(x), aucune collision avec la plage tenant. |
|
||||
| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | note : serveur_powerdns declare un port `derive` que le plan du site ne resout pas — aucune regle emise. |
|
||||
| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 4 tenant(s), 56 groupe(s), 104 regle(s). |
|
||||
| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 13 hote(s) x 6 integration(s) universelle(s) : aucune lacune, aucune recopie (0 exemption(s) derivee(s) du service rendu). |
|
||||
| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 8 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. |
|
||||
| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 5 pool(s) Proxmox, 44 VM placee(s), aucun nom ni VMID en collision. |
|
||||
| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 34 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 23, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv |
|
||||
| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 3 zone(s), 15 VNet(s), 15 sous-reseau(x), aucune collision. |
|
||||
| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 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). |
|
||||
| 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)). |
|
||||
| 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). |
|
||||
| P37 | Le placement du tenant existe chez son hebergeur | — | ✅ OK | placement confronte a l'hebergeur monte (SITE-Chezlepro) : noeud, stockage, pont — tous offerts. |
|
||||
| P38 | Catalogue des services : la carte dit ce que le moteur fait | — | ✅ OK | Catalogue a jour : 42 role(s) serveur/client tous nommes, 42 groupe(s) cite(s) en table existent tous. |
|
||||
| P39 | Glossaire : tout mot employe est enseigne | — | ✅ OK | Glossaire complet : 81 terme(s) du jargon expliques, 15 lien(s) valides, 96 page(s) de wiki toutes atteignables. |
|
||||
| P40 | Parente : l'ecosysteme sait de quoi il descend | — | ✅ OK | Parente coherente : 4 depot(s), tous retrouves, tous porteurs d'un remote. |
|
||||
| P41 | Resolution d'instance : une seule, partagee | — | ✅ OK | Resolution unique : 67 script(s) passent par `inventory_rules`, 3 exemption(s) nommee(s). |
|
||||
| P42 | L'edge porte les noms qu'il publie | — | ✅ OK | 5 edge(s) emettent un certificat portant les noms publies (instance-ci-1646753/production, OPS-Chezlepro-lab/principal, OPS-Chezlepro/principal, OPS-Technolibre |
|
||||
| P43 | Frontiere : le devis voit les machines du site | — | ✅ OK | Devis de la frontiere : 9 machine(s) du plan retrouvees, 192 regle(s) du site. |
|
||||
| P44 | Integrations : le serveur avant ses clients | — | ✅ OK | 5 integration(s) appliquent leur serveur avant leurs clients. |
|
||||
| P45 | Pare-feu Proxmox : arme sur les VNet SDN, jamais ailleurs | — | ✅ OK | Le pare-feu Proxmox ne s'arme que sur un VNet SDN (4 cas evalues, dont un qui doit rendre VRAI). |
|
||||
| P46 | Plancher /etc/hosts : un seul role en decide | — | ✅ OK | Un seul maitre du plancher — roles/hosts_statiques/tasks/main.yml : manage_etc_hosts: false ; et le gabarit maitre est pose (roles/hosts_statiques/templates/hos |
|
||||
| P47 | Zones inverses : couvrir l'occupe, et rien de plus | — | ✅ OK | Les zones inverses couvrent l'occupe et rien de plus (5 cas evalues, dont un site a quatre zones et un tenant a une). |
|
||||
| P48 | La carte d'orientation designe ce qui existe, et compte juste | — | ✅ OK | La carte designe 97 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). |
|
||||
| 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. |
|
||||
| P51 | Collections : toutes declarees, toutes epinglees | — | ✅ OK | 3 collection(s) et 2 bibliotheque(s) Python declarees et epinglees : ansible.posix==1.6.2, community.general==10.3.0, community.postgresql==3.10.2 |
|
||||
| P52 | Materialiser n'exige pas d'entrer dans le tenant | — | ✅ OK | `creer-vm` confirme par l'agent invite (API des hyperviseurs, deja utilisee pour creer), sans exiger d'entrer dans le tenant. |
|
||||
| P53 | L'interne refuse a voix haute, la bordure se tait | — | ✅ OK | L'interne parle, la bordure se tait — 13 ruleset(s) nftables refusent a voix haute ; pare-feu est-ouest en REJECT, source unique ; frontiere muette (actions : b |
|
||||
| P54 | L'insemination ne reclame aucun secret du tenant | — | ✅ OK | 2 couche(s) d'insemination (serveur_debian, serveur_ops), 9 role(s) applique(s), aucun secret de tenant reclame. |
|
||||
| P55 | La cle du SITE ne nait que sur le runner d'un tenant | — | ✅ OK | 13 hote(s) : la cle du SITE ne nait que sur 1 runner(s) de tenant, celle du tenant sur 13. |
|
||||
| P56 | Gabarit minimal, et rien de retire n'est perdu | — | ✅ OK | Gabarit minimal : 4 role(s), tous indispensables au premier demarrage ; 14 role(s) retire(s), tous repris par le socle ou le durcissement. |
|
||||
| P57 | Comptes en prose : les chiffres du depot sur lui-meme | — | ✅ OK | Les comptes ecrits en prose correspondent a la mesure (82 preuves, 69 roles, 42 groupes). |
|
||||
| P58 | Habilitations : chaque service dit a quel GROUPE, et par quoi | — | ✅ OK | 8 habilitation(s) declarees, toutes nommant un groupe, un mecanisme connu et une raison ; les `role-realm` sont projetees. |
|
||||
| P59 | Enumerations annoncees : le nombre correspond a ce qui suit | — | ✅ OK | 2 enumeration(s) annoncee(s) correspondent a ce qu'elles annoncent (formes non ambigues seulement). |
|
||||
| P60 | Wiki publie : la forge sert ce que le depot dit | AFF-002 | ✅ OK | Le wiki publie correspond au depot : `wiki/` n'a pas bouge depuis `392da4d` (publie le 2026-09-14). |
|
||||
| P61 | Schema du plan : il decrit tout ce que les plans contiennent | AFF-033 | ✅ OK | Le schema decrit 52 champ(s) sur 6 registres ; il couvre tout ce que les plans reels contiennent, et la FORME de chaque champ (scalaire / objet / table) corresp |
|
||||
| P62 | Schema du plan : il decrit tout ce que le MOTEUR accepte | AFF-033 | ✅ OK | Les 4 validateurs n'acceptent aucun champ que le schema ignore (applications:8, bases_donnees:4, domaines_publics:8, serveurs:3 champ(s) lus par validateur). |
|
||||
| P63 | cloud-init nait avec la VM et ne lui survit pas | — | ✅ OK | cloud-init est au gabarit (la premiere seconde), absent du socle (pas de va-et-vient), et retire par le durcissement — avec la garde qui verifie que le reseau s |
|
||||
| P64 | Sondes de supervision : declarees ET deposees | — | ✅ OK | 41 sonde(s) declaree(s) ET deposee(s), chacune avec sa raison et son `ttl` : client_journal/journaux, client_metrique/metriques, client_pki/certificat, serveur_ |
|
||||
| P65 | Depots tiers : demandes au cache, jamais en HTTPS direct | — | ✅ OK | 4 depot(s) tiers relaye(s) par le cache, aucun role ne les vise en https:// ecrit en dur. |
|
||||
| P66 | Clients OIDC : chaque URI vise un nom que le plan expose | — | ✅ OK | 4 client(s) OIDC, toutes leurs URI visent un FQDN que le plan expose (6 exposition(s)). |
|
||||
| P67 | Nom public : le service porte celui du plan, pas celui du role | — | ✅ OK | 13 service(s) expose(s) portent le nom du plan, sur 2 inventaire(s) : instance, SITE. |
|
||||
| P68 | Cle de depot telechargee : mesuree avant d'etre utilisee | — | ✅ OK | 5 role(s) telechargent une cle de depot, tous la mesurent avant de s'en servir. |
|
||||
| P69 | Amorcage d'un tenant : l'adresse designe le site REEL | — | ✅ OK | 2 adresse(s) d'amorcage designent bien une machine du site. |
|
||||
| P70 | Depot de binaires : il tient tout ce que les roles vont chercher | — | ✅ OK | 6 artefact(s) direct(s) tenus par le depot du site. |
|
||||
| P71 | Pool du site : le genome ne nait pas chez un tenant | — | ✅ OK | `site-creer` nomme `--pool-site` ; le pool du genome ne peut plus etre celui d'un tenant. |
|
||||
| P72 | Annuaire : aucun service ne se lie avec le compte du maitre | — | ✅ OK | 5 role(s) consultent l'annuaire, chacun avec SON compte de service ; seul `amorcage_acces` garde celui d'administration, et il provisionne au lieu de consommer. |
|
||||
| P73 | Le locataire designe les services de son site REEL | — | ✅ OK | CONFORME : 9 intrant(s) du locataire concordent avec ce que le site expose (1 non declare(s), donc derive(s) ou non utilise(s)). |
|
||||
| P74 | Gabarit dore : une seule declaration, au plan du site | — | ✅ OK | Gabarit declare une seule fois : VMID 9006 « modeleSetOPS-minimal », precedent 99998. |
|
||||
| P75 | Les parametres de clonage traversent les trois maillons | — | ✅ OK | 14 parametre(s) de clonage, tous emis par l'inventaire. |
|
||||
| P76 | Tout gabarit de role se rend vraiment | — | ✅ OK | 165 gabarits de role : tous se rendent. |
|
||||
| P77 | Panneaux declares : assemblables, et gradues | — | ✅ OK | 8 panneau(x) declare(s) dans 2 role(s), tous avec titre, expression, raison et une unite que la table sait traduire. |
|
||||
| P78 | Un consommateur de base suit le verrou TLS de son serveur | — | ✅ OK | 3 consommateur(s) suivent la posture de leur serveur ; 2 sans reglage TLS (serveur_icingaweb2, serveur_nextcloud). |
|
||||
| P79 | Replis silencieux : une derivation vide ne passe pas pour un succes | — | ✅ OK | 6 ecosysteme(s) (instance-ci-1646753, OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre, OPS-Patient0, SITE-Chezlepro) : pattes, edges, certificats, rechargemen |
|
||||
| P80 | Remise au client : inscrite, nommee, et son second temps a l'heure | — | ✅ OK | Aucun ecosysteme remis a un client : rien a tenir. |
|
||||
| P81 | La console dit sa portee, et ne sert pas un inventaire vide en silence | — | ✅ OK | Portee `poste` derivee des symlinks, source d'inventaire `instance`, et 14 route(s) POST exigent toutes un pouvoir. |
|
||||
| P82 | DNS public : les zones publiees sont servies, signees avant d'etre exposees | — | ✅ OK | 2 zone(s) publique(s) declaree(s), toutes servies par le site ; 2 replication(s) declaree(s) des deux cotes ; transfert ouvert a la cle seule ; aucune expositio |
|
||||
|
||||
## Couverture des affirmations ✅ du registre
|
||||
|
||||
Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus.
|
||||
Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005
|
||||
`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite
|
||||
d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ;
|
||||
elles restent hors du harnais recurrent (rien d'executable a rejouer).
|
||||
|
||||
## Declarations d'intention (⚪ invérifiables localement — assumees)
|
||||
|
||||
Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees**
|
||||
comme declarations d'intention, non comme preuves :
|
||||
|
||||
- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement
|
||||
contre une flotte vivante.
|
||||
- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles.
|
||||
- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee.
|
||||
- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume.
|
||||
|
||||
_Rapport genere le 2026-09-17._
|
||||
|
|
@ -249,3 +249,31 @@ que la sonde accepte en paramètre — jamais en substituant l'artefact que d'au
|
|||
consomment. Quand c'est impossible, on le fait sur la machine la moins critique, on
|
||||
l'annonce, et on vérifie l'état des consommateurs **après**, pas seulement celui de la
|
||||
sonde.
|
||||
|
||||
## Le matériel : un catalogue, deux lecteurs (2026-09-17)
|
||||
|
||||
Les hyperviseurs et la frontière n'ont aucun rôle Set-OPS pour déclarer leurs capteurs, et
|
||||
ils ne peuvent rien pousser. Prometheus les **tire** déjà ; on juge donc ce qu'il a récolté.
|
||||
|
||||
`scripts/materiel.py` est la seule source : familles de températures (processeur, carte
|
||||
mère, NVMe, disques SATA, DDR5, cartes réseau, frontière) avec leurs seuils et leur raison,
|
||||
et contrôles de santé (ventilateur arrêté, SMART en échec, secteurs défaillants ou **en
|
||||
progression**, alerte et usure NVMe, relevés SMART périmés). Deux lecteurs :
|
||||
|
||||
| lecteur | ce qu'il en fait |
|
||||
|---|---|
|
||||
| Grafana — tableau « Set-OPS — Matériel » | tuiles colorées aux seuils, courbes avec seuils tracés, ventilateurs, puissance, usure et âge des disques |
|
||||
| Icinga — hôte par équipement, service `materiel` | `check_materiel.py` lit Prometheus avec les mêmes seuils ; l'hôte est jugé par `up`, pas par un ping qu'aucun flux ne permet |
|
||||
|
||||
Règles apprises en le construisant :
|
||||
|
||||
- **Le matériel ment sur ses limites.** Le Super I/O annonce 125 °C « critique » pour la carte
|
||||
mère : les seuils sont déclarés, avec leur raison.
|
||||
- **Un connecteur vide n'est pas un ventilateur arrêté** : on ne crie que pour un ventilateur
|
||||
qui a tourné dans les 24 h.
|
||||
- **La progression, pas le compte.** Un disque aux secteurs défaillants stables est un
|
||||
avertissement ; c'est un compte qui **monte** qui est critique. Une alarme toujours rouge
|
||||
est une alarme qu'on cesse de lire.
|
||||
- **Le côté droit d'une jointure PromQL est agrégé.** Un réétiquetage laisse cinq minutes
|
||||
deux séries pour une même clé, et la requête est refusée.
|
||||
- **Une requête refusée n'est pas un Prometheus injoignable** : la sonde rapporte l'erreur.
|
||||
|
|
|
|||
|
|
@ -318,6 +318,23 @@
|
|||
tableau_panneaux: "{{ item.1.panneaux }}"
|
||||
notify: Redemarrer grafana
|
||||
|
||||
# --- LE MATERIEL (2026-09-17) -----------------------------------------------------------
|
||||
#
|
||||
# Les hyperviseurs n'ont aucun role Set-OPS pour declarer leurs capteurs : ils ne sont pas
|
||||
# de la flotte. Le catalogue vient donc de l'inventaire du site (`scripts/materiel.py`), et
|
||||
# c'est le meme que la sonde Icinga lit. Pas de prefixe `setops-role-` : la moisson des
|
||||
# tableaux de roles orphelins ne doit pas le voir.
|
||||
- name: Assembler le tableau du matériel
|
||||
ansible.builtin.template:
|
||||
src: tableau-materiel.json.j2
|
||||
dest: "{{ serveur_grafana_dashboards_dir }}/setops-materiel.json"
|
||||
owner: root
|
||||
group: grafana
|
||||
mode: "0640"
|
||||
validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s"
|
||||
when: setops_materiel is defined
|
||||
notify: Redemarrer grafana
|
||||
|
||||
# --- UN ROLE QU'ON RETIRE DOIT POUVOIR DEFAIRE CE QU'IL A FAIT ------------------------
|
||||
#
|
||||
# Meme lecon que les sondes orphelines de `client_sante`, et elle s'est deja payee ici :
|
||||
|
|
|
|||
206
roles/serveur_grafana/templates/tableau-materiel.json.j2
Normal file
206
roles/serveur_grafana/templates/tableau-materiel.json.j2
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
{#- GENERE par Set-OPS (role serveur_grafana). Ne pas editer a la main.
|
||||
|
||||
LE TABLEAU DU MATERIEL — hyperviseurs et frontiere (2026-09-17).
|
||||
|
||||
Assemble depuis `scripts/materiel.py`, le MEME catalogue que la sonde Icinga lit : un
|
||||
seuil change la-bas change la couleur ici et le verdict la-bas. Deux ecritures
|
||||
divergeraient, et un graphe rouge cohabiterait avec une supervision verte.
|
||||
|
||||
Le JSON est construit comme une structure Jinja puis rendu d'un bloc par `tojson` :
|
||||
aucune virgule a la main, donc aucun tableau que Grafana sauterait en silence.
|
||||
-#}
|
||||
{%- set cat = setops_materiel -%}
|
||||
{%- set filtre = ' and on(hote) up{job=~"fabric|frontiere",hote=~"$hote"}' -%}
|
||||
{%- set panneaux = [] -%}
|
||||
{%- set y = namespace(v=0, id=0) -%}
|
||||
|
||||
{%- macro seuils(f) -%}
|
||||
{{ {"mode": "absolute", "steps": [
|
||||
{"color": "green", "value": none},
|
||||
{"color": "orange", "value": f.avert},
|
||||
{"color": "red", "value": f.crit}]} | tojson }}
|
||||
{%- endmacro -%}
|
||||
|
||||
{#- ---- RANGEE : ETAT EN DIRECT ------------------------------------------------------ #}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "État en direct", "collapsed": false,
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
||||
{%- set y.v = y.v + 1 -%}
|
||||
{%- set tuiles = cat.temperatures | selectattr("cle", "in", ["processeur", "carte_mere", "nvme", "disques"]) | list -%}
|
||||
{%- for f in tuiles -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "stat", "id": y.id,
|
||||
"title": f.titre ~ " — maximum",
|
||||
"description": f.raison,
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 5, "w": 6, "x": loop.index0 * 6, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "celsius", "decimals": 0,
|
||||
"thresholds": (seuils(f) | from_json)}, "overrides": []},
|
||||
"options": {"colorMode": "background", "graphMode": "area", "justifyMode": "center",
|
||||
"textMode": "value_and_name", "orientation": "vertical",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "max by (hote) ((" ~ f.expr_juge ~ ")" ~ filtre ~ ")",
|
||||
"legendFormat": "{{hote}}"}]}) -%}
|
||||
{%- endfor -%}
|
||||
{%- set y.v = y.v + 5 -%}
|
||||
|
||||
{#- Les controles : une tuile chacun. Zero = vert ; sinon la couleur de SON niveau. #}
|
||||
{%- set largeur = [24 // (cat.controles | length), 3] | max -%}
|
||||
{%- for c in cat.controles -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "stat", "id": y.id,
|
||||
"title": c.titre,
|
||||
"description": c.raison,
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 4, "w": largeur, "x": (loop.index0 * largeur) % 24, "y": y.v + ((loop.index0 * largeur) // 24) * 4},
|
||||
"fieldConfig": {"defaults": {"unit": "none", "decimals": 0,
|
||||
"noValue": "0",
|
||||
"thresholds": {"mode": "absolute", "steps": [
|
||||
{"color": "green", "value": none},
|
||||
{"color": ("red" if c.niveau == "crit" else "orange"), "value": 1}]}},
|
||||
"overrides": []},
|
||||
"options": {"colorMode": "background", "graphMode": "none", "justifyMode": "center",
|
||||
"textMode": "value", "orientation": "auto",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "count(count by (hote, capteur) ((" ~ c.expr ~ ")" ~ filtre ~ ")) or vector(0)"}]}) -%}
|
||||
{%- endfor -%}
|
||||
{%- set y.v = y.v + (((cat.controles | length) * largeur + 23) // 24) * 4 -%}
|
||||
|
||||
{#- ---- RANGEE : TEMPERATURES -------------------------------------------------------- #}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Températures", "collapsed": false,
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
||||
{%- set y.v = y.v + 1 -%}
|
||||
{%- for f in cat.temperatures -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "timeseries", "id": y.id,
|
||||
"title": f.titre,
|
||||
"description": f.raison ~ " — Avertissement " ~ f.avert ~ " °C, critique " ~ f.crit ~ " °C (verdict Icinga lisse sur " ~ f.jugee_sur ~ ").",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 12, "x": 0 if loop.index0 % 2 == 0 else 12, "y": y.v + (loop.index0 // 2) * 9},
|
||||
"fieldConfig": {"defaults": {
|
||||
"unit": "celsius", "decimals": 1,
|
||||
"color": {"mode": "palette-classic"},
|
||||
"thresholds": (seuils(f) | from_json),
|
||||
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 8, "gradientMode": "opacity",
|
||||
"showPoints": "never", "spanNulls": true,
|
||||
"thresholdsStyle": {"mode": "dashed"}}},
|
||||
"overrides": []},
|
||||
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "max"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "(" ~ f.expr ~ ")" ~ filtre,
|
||||
"legendFormat": "{{hote}} · {{capteur}}"}]}) -%}
|
||||
{%- endfor -%}
|
||||
{%- set y.v = y.v + (((cat.temperatures | length) + 1) // 2) * 9 -%}
|
||||
|
||||
{#- ---- RANGEE : VENTILATEURS ET PUISSANCE ----------------------------------------------- #}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Ventilateurs et puissance", "collapsed": false,
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
||||
{%- set y.v = y.v + 1 -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "timeseries", "id": y.id, "title": "Ventilateurs",
|
||||
"description": "Seuls les connecteurs qui ont tourne dans les dernieres 24 h : un connecteur vide lit 0 depuis toujours et n'apprend rien.",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 12, "x": 0, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "rotrpm", "decimals": 0, "color": {"mode": "palette-classic"},
|
||||
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 5, "showPoints": "never", "spanNulls": true}},
|
||||
"overrides": []},
|
||||
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["lastNotNull", "min"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "(node_hwmon_fan_rpm{job=~\"fabric|frontiere\"} and on(instance, chip, sensor) (max_over_time(node_hwmon_fan_rpm{job=~\"fabric|frontiere\"}[1d]) > 0))" ~ filtre,
|
||||
"legendFormat": "{{hote}} · {{sensor}}"}]}) -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "timeseries", "id": y.id, "title": "Puissance du processeur (PPT)",
|
||||
"description": "Puissance du boitier processeur annoncee par le pilote amdgpu (APU integre). Elle suit la charge : une temperature qui monte SANS puissance qui monte parle du refroidissement.",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 12, "x": 12, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "watt", "decimals": 1, "color": {"mode": "palette-classic"},
|
||||
"custom": {"drawStyle": "line", "lineWidth": 2, "fillOpacity": 15, "gradientMode": "opacity", "showPoints": "never", "spanNulls": true}},
|
||||
"overrides": []},
|
||||
"options": {"legend": {"displayMode": "table", "placement": "bottom", "calcs": ["mean", "max"]},
|
||||
"tooltip": {"mode": "multi", "sort": "desc"}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "(node_hwmon_power_watt{job=~\"fabric|frontiere\"} * on(instance, chip, sensor) group_left(label) group by (instance, chip, sensor, label) (node_hwmon_sensor_label{job=~\"fabric|frontiere\",label=\"PPT\"}))" ~ filtre,
|
||||
"legendFormat": "{{hote}}"}]}) -%}
|
||||
{%- set y.v = y.v + 9 -%}
|
||||
|
||||
{#- ---- RANGEE : STOCKAGE ------------------------------------------------------------ #}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({"type": "row", "id": y.id, "title": "Stockage", "collapsed": false,
|
||||
"gridPos": {"h": 1, "w": 24, "x": 0, "y": y.v}}) -%}
|
||||
{%- set y.v = y.v + 1 -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "bargauge", "id": y.id, "title": "Usure des NVMe",
|
||||
"description": "Part de l'endurance annoncee deja consommee. Avertissement a 80 % : a commander, pas encore a remplacer.",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 8, "x": 0, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "percentunit", "min": 0, "max": 1, "decimals": 0,
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 0.8}, {"color": "red", "value": 0.9}]}},
|
||||
"overrides": []},
|
||||
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "nvme_percentage_used_ratio{job=~\"fabric|frontiere\"}" ~ filtre,
|
||||
"legendFormat": "{{hote}} · {{device}}"}]}) -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "bargauge", "id": y.id, "title": "Secteurs défaillants",
|
||||
"description": "En attente + irreparables + realloues, par disque SATA. Un compte stable se surveille ; un compte qui MONTE se remplace (controle « secteurs en progression »).",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 8, "x": 8, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "none", "min": 0, "decimals": 0,
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "green", "value": none}, {"color": "orange", "value": 1}, {"color": "red", "value": 100}]}},
|
||||
"overrides": []},
|
||||
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "(sum by (hote, disk) (smartmon_current_pending_sector_raw_value{job=~\"fabric|frontiere\"} or smartmon_offline_uncorrectable_raw_value{job=~\"fabric|frontiere\"} or smartmon_reallocated_sector_ct_raw_value{job=~\"fabric|frontiere\"}))" ~ filtre,
|
||||
"legendFormat": "{{hote}} · {{disk}}"}]}) -%}
|
||||
{%- set y.id = y.id + 1 -%}
|
||||
{%- set _ = panneaux.append({
|
||||
"type": "bargauge", "id": y.id, "title": "Heures de fonctionnement",
|
||||
"description": "Age reel des disques. Un disque mecanique au-dela de 40 000 heures (4,5 ans) entre dans la zone ou les pannes se multiplient.",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"gridPos": {"h": 9, "w": 8, "x": 16, "y": y.v},
|
||||
"fieldConfig": {"defaults": {"unit": "h", "min": 0, "decimals": 0,
|
||||
"thresholds": {"mode": "absolute", "steps": [{"color": "blue", "value": none}, {"color": "orange", "value": 40000}]}},
|
||||
"overrides": []},
|
||||
"options": {"displayMode": "gradient", "orientation": "horizontal", "showUnfilled": true, "valueMode": "color",
|
||||
"reduceOptions": {"calcs": ["lastNotNull"], "fields": "", "values": false}},
|
||||
"targets": [{"refId": "A", "datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"expr": "(label_replace(smartmon_power_on_hours_raw_value{job=~\"fabric|frontiere\",type=\"sat\"}, \"dispositif\", \"$1\", \"disk\", \"/dev/(.*)\") or label_replace(nvme_power_on_hours_total{job=~\"fabric|frontiere\"}, \"dispositif\", \"$1\", \"device\", \"(.*)\"))" ~ filtre,
|
||||
"legendFormat": "{{hote}} · {{dispositif}}"}]}) -%}
|
||||
|
||||
{{ {
|
||||
"uid": "setops-materiel",
|
||||
"title": "Set-OPS — Matériel",
|
||||
"description": "Hyperviseurs et frontiere : temperatures, ventilateurs, puissance, disques. Meme catalogue et memes seuils que le service Icinga « materiel » (scripts/materiel.py).",
|
||||
"tags": ["set-ops", "derive", "materiel", "fabric"],
|
||||
"timezone": "browser",
|
||||
"refresh": "1m",
|
||||
"time": {"from": "now-24h", "to": "now"},
|
||||
"schemaVersion": 39,
|
||||
"version": 1,
|
||||
"graphTooltip": 1,
|
||||
"templating": {"list": [
|
||||
{"name": "ds_prom", "label": "Source Prometheus", "type": "datasource", "query": "prometheus", "hide": 2, "current": {}},
|
||||
{"name": "hote", "label": "Équipement", "type": "query",
|
||||
"datasource": {"type": "prometheus", "uid": "${ds_prom}"},
|
||||
"query": {"query": "label_values(up{job=~\"fabric|frontiere\"}, hote)", "refId": "hote"},
|
||||
"definition": "label_values(up{job=~\"fabric|frontiere\"}, hote)",
|
||||
"multi": true, "includeAll": true, "current": {"selected": true, "text": ["All"], "value": ["$__all"]},
|
||||
"refresh": 2, "sort": 1}
|
||||
]},
|
||||
"panels": panneaux
|
||||
} | tojson }}
|
||||
|
|
@ -117,6 +117,12 @@ serveur_icinga_hotes_supervises: >-
|
|||
serveur_icinga_hotes_conf: "/etc/icinga2/conf.d/setops-hotes.conf"
|
||||
serveur_icinga_sante_conf: "/etc/icinga2/conf.d/setops-sante.conf"
|
||||
serveur_icinga_sondes_conf: "/etc/icinga2/conf.d/setops-sondes.conf"
|
||||
# LE MATERIEL (2026-09-17) : catalogue et hotes viennent de l'inventaire du site
|
||||
# (`setops_materiel`, `setops_materiel_hotes`) ; absents, rien n'est pose.
|
||||
serveur_icinga_materiel_conf: "/etc/icinga2/conf.d/setops-materiel.conf"
|
||||
serveur_icinga_materiel_catalogue: "/etc/icinga2/setops-materiel.json"
|
||||
serveur_icinga_materiel_sonde: "/usr/local/lib/setops/icinga/check_materiel.py"
|
||||
serveur_icinga_materiel_prometheus: "http://127.0.0.1:9090"
|
||||
# Rempli a l'execution depuis les `meta/supervision.yml` des roles.
|
||||
serveur_icinga_sondes: {}
|
||||
|
||||
|
|
|
|||
150
roles/serveur_icinga/files/check_materiel.py
Normal file
150
roles/serveur_icinga/files/check_materiel.py
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Verdict materiel d'un equipement — Icinga le lance, Prometheus lui repond.
|
||||
|
||||
GERE par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
||||
|
||||
POURQUOI UN CONTROLE ACTIF QUI LIT PROMETHEUS (2026-09-17). Les hyperviseurs ne peuvent rien
|
||||
pousser (route par defaut gelee, D-57) et la frontiere n'accueille aucun agent. Prometheus,
|
||||
lui, les TIRE deja. On juge donc ce qu'il a recolte, avec le catalogue que le tableau
|
||||
Grafana affiche (`scripts/materiel.py`) : memes capteurs, memes seuils.
|
||||
|
||||
UNE MESURE QUI ECHOUE N'EST PAS UN MATERIEL SAIN. Prometheus injoignable, ou aucune serie
|
||||
pour l'equipement : UNKNOWN, jamais OK.
|
||||
|
||||
Usage :
|
||||
check_materiel.py --hote asgard --catalogue /etc/icinga2/setops-materiel.json
|
||||
check_materiel.py --hote asgard --catalogue ... --joignabilite # controle d'hote
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
OK, AVERT, CRIT, INCONNU = 0, 1, 2, 3
|
||||
ETATS = {OK: "OK", AVERT: "AVERTISSEMENT", CRIT: "CRITIQUE", INCONNU: "INCONNU"}
|
||||
JOBS = 'job=~"fabric|frontiere"'
|
||||
|
||||
|
||||
class Muette(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def requete(prom: str, expr: str) -> list:
|
||||
url = prom.rstrip("/") + "/api/v1/query?" + urllib.parse.urlencode({"query": expr})
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=15) as r:
|
||||
rep = json.load(r)
|
||||
except urllib.error.HTTPError as exc:
|
||||
# UNE REQUETE REFUSEE N'EST PAS UN PROMETHEUS INJOIGNABLE (eprouve le 2026-09-17 : un
|
||||
# 422 a d'abord ete rapporte comme une panne de Prometheus). On dit ce qu'il repond.
|
||||
try:
|
||||
corps = json.load(exc)
|
||||
except ValueError:
|
||||
corps = {}
|
||||
raise Muette(f"Prometheus refuse une requete ({exc.code}) : "
|
||||
f"{corps.get('error', exc.reason)}") from exc
|
||||
except (OSError, ValueError) as exc:
|
||||
raise Muette(f"Prometheus injoignable ({prom}) : {exc}") from exc
|
||||
if rep.get("status") != "success":
|
||||
raise Muette(f"requete refusee par Prometheus : {rep.get('error', '?')}")
|
||||
return rep["data"]["result"]
|
||||
|
||||
|
||||
def joignabilite(prom: str, hote: str) -> int:
|
||||
series = requete(prom, f'up{{{JOBS},hote="{hote}"}}')
|
||||
if not series:
|
||||
print(f"INCONNU — Prometheus ne connait aucune cible nommee « {hote} ».")
|
||||
return INCONNU
|
||||
if all(float(s["value"][1]) == 1 for s in series):
|
||||
print(f"OK — {hote} repond a Prometheus.")
|
||||
return OK
|
||||
print(f"CRITIQUE — l'exportateur de {hote} ne repond plus a Prometheus : equipement "
|
||||
f"eteint, injoignable, ou exportateur arrete.")
|
||||
return CRIT
|
||||
|
||||
|
||||
def verdict(prom: str, hote: str, cat: dict) -> int:
|
||||
code = joignabilite_silencieuse(prom, hote)
|
||||
if code is not None:
|
||||
return code
|
||||
|
||||
etat = OK
|
||||
lignes, perf, resume = [], [], []
|
||||
for f in cat["temperatures"]:
|
||||
expr = (f'max by (hote, capteur) (avg_over_time(({f["expr_juge"]})'
|
||||
f'[{f["jugee_sur"]}:1m]))')
|
||||
mesures = sorted(
|
||||
(s["metric"].get("capteur", "?"), float(s["value"][1]))
|
||||
for s in requete(prom, expr) if s["metric"].get("hote") == hote)
|
||||
if not mesures:
|
||||
continue # famille sans capteur sur cet equipement : ni panne ni oubli
|
||||
pire = max(v for _, v in mesures)
|
||||
niveau = CRIT if pire >= f["crit"] else AVERT if pire >= f["avert"] else OK
|
||||
etat = max(etat, niveau)
|
||||
detail = ", ".join(f"{c} {v:.0f} °C" for c, v in mesures)
|
||||
lignes.append(f"[{ETATS[niveau]}] {f['titre']} : {detail} "
|
||||
f"(seuils {f['avert']}/{f['crit']} °C, moyenne {f['jugee_sur']})")
|
||||
if niveau != OK:
|
||||
resume.append(f"{f['titre']} {pire:.0f} °C")
|
||||
for c, v in mesures:
|
||||
perf.append(f"'{f['cle']} {c}'={v:.1f};{f['avert']};{f['crit']}")
|
||||
|
||||
for c in cat["controles"]:
|
||||
defauts = sorted({s["metric"].get("capteur", "?")
|
||||
for s in requete(prom, c["expr"]) if s["metric"].get("hote") == hote})
|
||||
if not defauts:
|
||||
continue
|
||||
niveau = CRIT if c["niveau"] == "crit" else AVERT
|
||||
etat = max(etat, niveau)
|
||||
lignes.append(f"[{ETATS[niveau]}] {c['titre']} : {', '.join(defauts)} — {c['raison']}")
|
||||
resume.append(f"{c['titre']} ({', '.join(defauts)})")
|
||||
|
||||
if not perf:
|
||||
print(f"INCONNU — aucun capteur de temperature relu pour {hote} : le catalogue ne "
|
||||
f"correspond a rien sur cet equipement.")
|
||||
return INCONNU
|
||||
tete = "; ".join(resume) if resume else f"{len(perf)} capteur(s) dans les seuils, aucun defaut disque ni ventilateur"
|
||||
print(f"{ETATS[etat]} — {tete} | {' '.join(perf)}")
|
||||
print("\n".join(lignes))
|
||||
return etat
|
||||
|
||||
|
||||
def joignabilite_silencieuse(prom: str, hote: str) -> int | None:
|
||||
series = requete(prom, f'up{{{JOBS},hote="{hote}"}}')
|
||||
if not series:
|
||||
print(f"INCONNU — Prometheus ne connait aucune cible nommee « {hote} ».")
|
||||
return INCONNU
|
||||
if not any(float(s["value"][1]) == 1 for s in series):
|
||||
print(f"INCONNU — l'exportateur de {hote} ne repond pas : aucun releve a juger "
|
||||
f"(le controle d'hote le dit).")
|
||||
return INCONNU
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--hote", required=True)
|
||||
ap.add_argument("--catalogue", required=True)
|
||||
ap.add_argument("--prometheus", default="http://127.0.0.1:9090")
|
||||
ap.add_argument("--joignabilite", action="store_true")
|
||||
a = ap.parse_args()
|
||||
try:
|
||||
if a.joignabilite:
|
||||
return joignabilite(a.prometheus, a.hote)
|
||||
with open(a.catalogue, encoding="utf-8") as fh:
|
||||
cat = json.load(fh)
|
||||
return verdict(a.prometheus, a.hote, cat)
|
||||
except Muette as exc:
|
||||
print(f"INCONNU — {exc}")
|
||||
return INCONNU
|
||||
except (OSError, ValueError, KeyError) as exc:
|
||||
print(f"INCONNU — catalogue illisible ou incomplet : {exc}")
|
||||
return INCONNU
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -367,6 +367,47 @@
|
|||
mode: "0640"
|
||||
notify: Redemarrer icinga2
|
||||
|
||||
# --- LE MATERIEL (2026-09-17) ------------------------------------------------------------
|
||||
# Hyperviseurs et frontiere : Icinga juge ce que Prometheus a tire, avec le catalogue du
|
||||
# tableau Grafana. Voir `scripts/materiel.py`.
|
||||
- name: Assurer le repertoire des controles actifs Set-OPS
|
||||
ansible.builtin.file:
|
||||
path: "{{ serveur_icinga_materiel_sonde | dirname }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: setops_materiel_hotes is defined
|
||||
|
||||
- name: Deposer le controle du materiel
|
||||
ansible.builtin.copy:
|
||||
src: check_materiel.py
|
||||
dest: "{{ serveur_icinga_materiel_sonde }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
when: setops_materiel_hotes is defined
|
||||
|
||||
- name: Deposer le catalogue des capteurs
|
||||
ansible.builtin.copy:
|
||||
content: "{{ setops_materiel | to_nice_json }}\n"
|
||||
dest: "{{ serveur_icinga_materiel_catalogue }}"
|
||||
owner: root
|
||||
group: nagios
|
||||
mode: "0640"
|
||||
validate: "python3 -c \"import json,sys; json.load(open(sys.argv[1]))\" %s"
|
||||
when: setops_materiel_hotes is defined
|
||||
|
||||
- name: Deployer les objets de supervision du materiel
|
||||
ansible.builtin.template:
|
||||
src: setops-materiel.conf.j2
|
||||
dest: "{{ serveur_icinga_materiel_conf }}"
|
||||
owner: root
|
||||
group: nagios
|
||||
mode: "0640"
|
||||
when: setops_materiel_hotes is defined
|
||||
notify: Redemarrer icinga2
|
||||
|
||||
- name: Valider la configuration Icinga 2 avant de la rendre vivante
|
||||
ansible.builtin.command:
|
||||
cmd: icinga2 daemon -C
|
||||
|
|
|
|||
54
roles/serveur_icinga/templates/setops-materiel.conf.j2
Normal file
54
roles/serveur_icinga/templates/setops-materiel.conf.j2
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Gere par Set-OPS (role serveur_icinga). Ne pas editer a la main.
|
||||
*
|
||||
* LE MATERIEL — hyperviseurs et frontiere (2026-09-17).
|
||||
*
|
||||
* Ces equipements n'accueillent aucun porteur de sante : ils ne peuvent rien pousser. Mais
|
||||
* Prometheus les TIRE deja. Le controle lit donc ce qu'il a recolte, avec le catalogue que
|
||||
* le tableau Grafana « Materiel » affiche — `scripts/materiel.py`, une source, deux lecteurs.
|
||||
*
|
||||
* L'HOTE EST JUGE PAR PROMETHEUS, PAS PAR UN PING. Un `hostalive` depuis cette machine vers
|
||||
* la grappe de controle exigerait un flux ICMP qu'aucune couche ne declare ; il dirait
|
||||
* « eteint » d'une machine en parfaite sante. Le seul chemin qui existe est celui de la
|
||||
* scrutation : c'est lui qu'on interroge.
|
||||
*/
|
||||
|
||||
object CheckCommand "setops-materiel" {
|
||||
command = [ "{{ serveur_icinga_materiel_sonde }}" ]
|
||||
arguments = {
|
||||
"--hote" = "$setops_materiel_hote$"
|
||||
"--catalogue" = "{{ serveur_icinga_materiel_catalogue }}"
|
||||
"--prometheus" = "{{ serveur_icinga_materiel_prometheus }}"
|
||||
}
|
||||
}
|
||||
|
||||
object CheckCommand "setops-materiel-joignable" {
|
||||
command = [ "{{ serveur_icinga_materiel_sonde }}", "--joignabilite" ]
|
||||
arguments = {
|
||||
"--hote" = "$setops_materiel_hote$"
|
||||
"--catalogue" = "{{ serveur_icinga_materiel_catalogue }}"
|
||||
"--prometheus" = "{{ serveur_icinga_materiel_prometheus }}"
|
||||
}
|
||||
}
|
||||
|
||||
{% for h in setops_materiel_hotes | default([]) %}
|
||||
object Host "{{ h.nom }}" {
|
||||
check_command = "setops-materiel-joignable"
|
||||
address = "{{ h.adresse }}"
|
||||
check_interval = 1m
|
||||
retry_interval = 30s
|
||||
vars.role = "{{ h.role }}"
|
||||
vars.setops_materiel_hote = "{{ h.nom }}"
|
||||
}
|
||||
|
||||
object Service "materiel" {
|
||||
host_name = "{{ h.nom }}"
|
||||
check_command = "setops-materiel"
|
||||
check_interval = 5m
|
||||
retry_interval = 1m
|
||||
{# Trois lectures avant de crier : le verdict est deja lisse (moyennes de 5 a 15 min), on
|
||||
ne veut pas d'une alerte sur un redemarrage de Prometheus. #}
|
||||
max_check_attempts = 3
|
||||
vars.setops_materiel_hote = "{{ h.nom }}"
|
||||
}
|
||||
{% endfor %}
|
||||
|
|
@ -56,5 +56,15 @@ scrape_configs:
|
|||
|
||||
- job_name: {{ job.job }}
|
||||
static_configs:
|
||||
{% if job.hotes | default({}) %}
|
||||
{# UNE CIBLE NOMMEE PAR BLOC : l'etiquette `hote` s'attache a un `static_config`, pas a
|
||||
une adresse. Les series disent alors « asgard », pas « 192.168.11.41:9100 ». #}
|
||||
{% for cible in job.cibles %}
|
||||
- targets: {{ [cible] | to_json }}
|
||||
labels:
|
||||
hote: {{ job.hotes[cible] | default(cible) | to_json }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
- targets: {{ job.cibles | to_json }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
|
|
|||
223
scripts/materiel.py
Normal file
223
scripts/materiel.py
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Le catalogue des capteurs materiels — une source, deux lecteurs : Grafana et Icinga.
|
||||
|
||||
POURQUOI UN CATALOGUE (2026-09-17). Les hyperviseurs exposaient deja 68 temperatures, leurs
|
||||
ventilateurs, leurs disques SMART et l'usure de leurs NVMe — collectes par Prometheus depuis
|
||||
le 2026-09-10, regardes par personne. Le tableau de bord et la sonde doivent lire les MEMES
|
||||
capteurs avec les MEMES seuils : ecrits deux fois, ils divergeraient, et un graphe rouge
|
||||
cohabiterait avec une supervision verte.
|
||||
|
||||
CE QUE LE MATERIEL DIT DE LUI-MEME, ET POURQUOI ON NE LE CROIT PAS. Les puces annoncent
|
||||
leurs propres limites (`temp_max`, `temp_crit`). Sur ces cartes ASUS elles valent 80 et 125 °C
|
||||
pour la carte mere — des valeurs d'usine du Super I/O, pas des limites du systeme. Les seuils
|
||||
sont donc DECLARES ici, avec leur raison.
|
||||
|
||||
CE QU'ON IGNORE, ET POURQUOI (releve du 2026-09-17 sur asgard, gandalf, vishnu) :
|
||||
- `AUXTIN0..5` du nct6799 : entrees non cablees (-62, 0, 12 °C) ;
|
||||
- `PCH_*` : lisent 0 sur ces cartes ;
|
||||
- `TSI0_TEMP` et `amdgpu edge` : le meme silicium que `Tctl`, vu autrement ;
|
||||
- la puce Wi-Fi : sans interet pour un hyperviseur ;
|
||||
- les disques iSCSI (`type="scsi"`) : des volumes distants, dont le materiel n'est pas ici.
|
||||
|
||||
Les familles sans capteur sur un hote (DDR5 et cartes reseau n'existent que sur le noyau 6.14
|
||||
de vishnu) ne rendent simplement aucune serie : ce n'est ni une panne ni un oubli.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# Les jobs Prometheus qui portent du materiel. `fabric` : les hyperviseurs ; `frontiere` :
|
||||
# le boitier OPNsense, quand son exportateur est en service.
|
||||
JOBS = 'job=~"fabric|frontiere"'
|
||||
|
||||
|
||||
# LE COTE DROIT D'UNE JOINTURE EST TOUJOURS AGREGE (eprouve le 2026-09-17). Le jour ou l'on a
|
||||
# ajoute l'etiquette `hote` aux cibles, Prometheus a garde cinq minutes la serie sans elle ET
|
||||
# la serie avec : deux series pour une meme cle de jointure, et la requete refusee (« many-to-
|
||||
# many matching not allowed »). Le tableau se vidait, la sonde rendait INCONNU. `group by` /
|
||||
# `max by` ramene la cle a une seule serie, quel que soit l'historique des etiquettes.
|
||||
def _hwmon(chip_name: str, etiquettes: str | None = None) -> str:
|
||||
"""Temperatures hwmon d'une puce, avec le libelle du capteur quand la puce en donne un."""
|
||||
expr = f"node_hwmon_temp_celsius{{{JOBS}}}"
|
||||
if etiquettes:
|
||||
expr = (f"({expr} * on(instance, chip, sensor) group_left(label) "
|
||||
f'group by (instance, chip, sensor, label) '
|
||||
f'(node_hwmon_sensor_label{{{JOBS},label=~"{etiquettes}"}}))')
|
||||
return (f"({expr} * on(instance, chip) group_left(chip_name) "
|
||||
f'group by (instance, chip, chip_name) '
|
||||
f'(node_hwmon_chip_names{{{JOBS},chip_name=~"{chip_name}"}}))')
|
||||
|
||||
|
||||
# UNE FAMILLE = une question qu'on se pose devant un equipement chaud.
|
||||
# expr : les series (au moins les etiquettes `hote` et `capteur`)
|
||||
# avert/crit: °C ; `None` = regardee, jamais jugee
|
||||
# jugee_sur : la fenetre de lissage du verdict — un pic d'une seconde n'est pas une panne
|
||||
TEMPERATURES = [
|
||||
{
|
||||
"cle": "processeur",
|
||||
"titre": "Processeur",
|
||||
"expr": f'label_replace({_hwmon("k10temp", "Tctl|Tccd1|Tccd2")}, "capteur", "$1", "label", "(.*)")',
|
||||
"juge": 'capteur="Tctl"',
|
||||
# Le verdict ne regarde que Tctl : les Tccd sont des sondes de coeur, plus bruitees.
|
||||
"expr_juge": f'label_replace({_hwmon("k10temp", "Tctl")}, "capteur", "$1", "label", "(.*)")',
|
||||
"avert": 90, "crit": 95, "jugee_sur": "5m",
|
||||
"raison": ("Ryzen 9 7900X : il est CONCU pour monter a 95 °C sous charge soutenue et s'y "
|
||||
"brider. Un Tctl a 95 pendant cinq minutes veut dire qu'il bride — la machine "
|
||||
"rend moins que ce qu'on croit. Au-dessus, c'est le refroidissement."),
|
||||
},
|
||||
{
|
||||
"cle": "carte_mere",
|
||||
"titre": "Carte mère",
|
||||
"expr": (f'label_replace({_hwmon("nct6799", "SYSTIN|CPUTIN")}, "capteur", "$1", "label", '
|
||||
f'"(.*)")'),
|
||||
"juge": 'capteur="SYSTIN"',
|
||||
"expr_juge": f'label_replace({_hwmon("nct6799", "SYSTIN")}, "capteur", "$1", "label", "(.*)")',
|
||||
"avert": 50, "crit": 60, "jugee_sur": "10m",
|
||||
"raison": ("SYSTIN est l'air du boitier. Il monte avec la piece, pas avec la charge : un "
|
||||
"SYSTIN qui grimpe sur les trois machines a la fois parle de la salle, pas "
|
||||
"d'une machine."),
|
||||
},
|
||||
{
|
||||
"cle": "nvme",
|
||||
"titre": "NVMe",
|
||||
"expr": (f'label_replace({_hwmon("nvme", "Composite")}, "capteur", "$1", "chip", '
|
||||
f'"nvme_(.*)")'),
|
||||
"juge": "",
|
||||
"avert": 70, "crit": 80, "jugee_sur": "5m",
|
||||
"raison": ("WD_BLACK SN850X : il ralentit ses ecritures vers 80-85 °C pour se proteger. "
|
||||
"Ceph et les VM le vivent comme une latence, sans aucune erreur."),
|
||||
},
|
||||
{
|
||||
"cle": "disques",
|
||||
"titre": "Disques SATA",
|
||||
# LES SSD SAMSUNG NE PORTENT PAS L'ATTRIBUT 194 : leur temperature est la 190
|
||||
# (« airflow »). Sans le repli, les deux 850 disparaissaient du tableau sans un mot.
|
||||
"expr": (f'label_replace((smartmon_temperature_celsius_raw_value{{{JOBS},type="sat"}} or '
|
||||
f'smartmon_airflow_temperature_cel_raw_value{{{JOBS},type="sat"}}), '
|
||||
f'"capteur", "$1", "disk", "/dev/(.*)")'),
|
||||
"juge": "",
|
||||
"avert": 50, "crit": 60, "jugee_sur": "15m",
|
||||
"raison": ("WD Red Plus 10 To (OSD HDD de Ceph) et SSD Samsung 850 : la vie d'un disque "
|
||||
"mecanique chute au-dessus de 50 °C, bien avant sa limite de 65. Celui de "
|
||||
"gandalf a deja vu 63 °C (maximum a vie, releve SMART du 2026-09-17)."),
|
||||
},
|
||||
{
|
||||
"cle": "memoire",
|
||||
"titre": "Mémoire DDR5",
|
||||
"expr": (f'label_replace({_hwmon("spd5118")}, "capteur", "barrette $1", "chip", '
|
||||
f'"i2c_(.*)")'),
|
||||
"juge": "",
|
||||
"avert": 70, "crit": 80, "jugee_sur": "5m",
|
||||
"raison": ("Capteur JEDEC des barrettes (noyau 6.14, vishnu seulement). La DDR5 "
|
||||
"commence a corriger des erreurs au-dela de 85 °C."),
|
||||
},
|
||||
{
|
||||
"cle": "reseau",
|
||||
"titre": "Cartes réseau",
|
||||
"expr": (f'label_replace({_hwmon("r8169.*")}, "capteur", "$1", "chip_name", '
|
||||
f'"(.*)")'),
|
||||
"juge": "",
|
||||
"avert": 95, "crit": 110, "jugee_sur": "5m",
|
||||
"raison": "Realtek RTL8125 (noyau 6.14, vishnu seulement) ; limite annoncee 120 °C.",
|
||||
},
|
||||
{
|
||||
"cle": "frontiere",
|
||||
"titre": "Frontière (cœurs)",
|
||||
"expr": (f'label_replace(node_cpu_temperature_celsius{{{JOBS}}}, "capteur", "cœur $1", '
|
||||
f'"cpu", "(.*)")'),
|
||||
"juge": "",
|
||||
"avert": 80, "crit": 90, "jugee_sur": "5m",
|
||||
"raison": ("Intel i3-5020U, Tjunction 100 °C. A 90, il bride — et c'est tout le trafic du "
|
||||
"site qui ralentit."),
|
||||
},
|
||||
]
|
||||
|
||||
# CE QUI NE SE MESURE PAS EN DEGRES. Chaque controle rend une expression dont TOUTE serie
|
||||
# presente est un defaut (etiquettes `hote` et `capteur`). Vide = sain.
|
||||
CONTROLES = [
|
||||
{
|
||||
"cle": "ventilateur_arrete",
|
||||
"titre": "Ventilateur arrêté",
|
||||
"niveau": "crit",
|
||||
# UN CONNECTEUR VIDE N'EST PAS UN VENTILATEUR ARRETE. fan4, fan6 et fan7 lisent 0
|
||||
# depuis toujours : on ne crie que pour un ventilateur qui A TOURNE dans les 24 h.
|
||||
"expr": (f'label_replace((node_hwmon_fan_rpm{{{JOBS}}} == 0) and on(instance, chip, sensor) '
|
||||
f'(max_over_time(node_hwmon_fan_rpm{{{JOBS}}}[1d]) > 300), "capteur", "$1", '
|
||||
f'"sensor", "(.*)")'),
|
||||
"raison": "Un ventilateur qui tournait hier et plus maintenant : la chaleur suit dans l'heure.",
|
||||
},
|
||||
{
|
||||
"cle": "smart_echec",
|
||||
"titre": "SMART : disque en échec",
|
||||
"niveau": "crit",
|
||||
"expr": (f'label_replace(smartmon_device_smart_healthy{{{JOBS},type=~"sat|nvme"}} == 0, '
|
||||
f'"capteur", "$1", "disk", "/dev/(.*)")'),
|
||||
"raison": "Le disque declare lui-meme qu'il va lacher. Il faut le remplacer, pas l'observer.",
|
||||
},
|
||||
{
|
||||
"cle": "secteurs_degradation",
|
||||
"titre": "Disque : secteurs en progression",
|
||||
"niveau": "crit",
|
||||
# LA PROGRESSION, PAS LE COMPTE. Le disque de gandalf porte 56 secteurs en attente,
|
||||
# stables depuis au moins une semaine, Ceph `HEALTH_OK` (releve du 2026-09-17). Un
|
||||
# verdict critique sur le compte serait rouge pour toujours — et une alarme toujours
|
||||
# rouge est une alarme qu'on cesse de lire. Ce qui presse, c'est qu'il AUGMENTE.
|
||||
"expr": (f'label_replace((delta(smartmon_current_pending_sector_raw_value{{{JOBS}}}[1d]) > 0) or '
|
||||
f'(delta(smartmon_reallocated_sector_ct_raw_value{{{JOBS}}}[1d]) > 0) or '
|
||||
f'(delta(smartmon_offline_uncorrectable_raw_value{{{JOBS}}}[1d]) > 0), '
|
||||
f'"capteur", "$1", "disk", "/dev/(.*)")'),
|
||||
"raison": "Le disque perd des secteurs EN CE MOMENT : sa degradation s'accelere, le remplacer.",
|
||||
},
|
||||
{
|
||||
"cle": "secteurs_defaillants",
|
||||
"titre": "Disque : secteurs défaillants",
|
||||
"niveau": "avert",
|
||||
"expr": (f'label_replace((smartmon_current_pending_sector_raw_value{{{JOBS}}} > 0) or '
|
||||
f'(smartmon_offline_uncorrectable_raw_value{{{JOBS}}} > 0) or '
|
||||
f'(smartmon_reallocated_sector_ct_raw_value{{{JOBS}}} > 0), "capteur", "$1", '
|
||||
f'"disk", "/dev/(.*)")'),
|
||||
"raison": ("Le disque a des secteurs illisibles ou deja remplaces. Stable, il tient ; "
|
||||
"a remplacer a la prochaine occasion."),
|
||||
},
|
||||
{
|
||||
"cle": "nvme_alerte",
|
||||
"titre": "NVMe : alerte du contrôleur",
|
||||
"niveau": "crit",
|
||||
"expr": (f'label_replace((nvme_critical_warning_total{{{JOBS}}} > 0) or '
|
||||
f'(nvme_available_spare_ratio{{{JOBS}}} < on(instance, device) group_left() '
|
||||
f'max by (instance, device) (nvme_available_spare_threshold_ratio{{{JOBS}}})), '
|
||||
f'"capteur", "$1", "device", "(.*)")'),
|
||||
"raison": "Le controleur NVMe signale un avertissement critique ou a epuise sa reserve.",
|
||||
},
|
||||
{
|
||||
"cle": "nvme_usure",
|
||||
"titre": "NVMe : usure > 80 %",
|
||||
"niveau": "avert",
|
||||
"expr": (f'label_replace(nvme_percentage_used_ratio{{{JOBS}}} > 0.8, "capteur", "$1", '
|
||||
f'"device", "(.*)")'),
|
||||
"raison": "L'endurance annoncee est presque consommee : a commander, pas encore a remplacer.",
|
||||
},
|
||||
{
|
||||
"cle": "donnees_perimees",
|
||||
"titre": "Relevés SMART périmés",
|
||||
"niveau": "avert",
|
||||
# UNE SONDE MUETTE N'EST PAS UNE SONDE SAINE. Les releves SMART et NVMe viennent de
|
||||
# minuteries (15 min) : si elles s'arretent, toutes les regles ci-dessus restent
|
||||
# vertes sur des chiffres figes.
|
||||
"expr": (f'label_replace(time() - node_textfile_mtime_seconds{{{JOBS},'
|
||||
f'file=~".*(smartmon|nvme)[.]prom"}} > 3600, "capteur", "$1", "file", '
|
||||
f'".*/(.*)[.]prom")'),
|
||||
"raison": "Les releves SMART/NVMe n'ont pas ete rafraichis depuis une heure : les verdicts ci-dessus sont figes.",
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def catalogue() -> dict:
|
||||
"""Ce que les roles recoivent (Grafana et Icinga). `expr_juge` vaut `expr` par defaut."""
|
||||
temperatures = [{**f, "expr_juge": f.get("expr_juge") or f["expr"]} for f in TEMPERATURES]
|
||||
for f in temperatures:
|
||||
f.pop("juge", None)
|
||||
return {"temperatures": temperatures, "controles": CONTROLES}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
print(json.dumps(catalogue(), ensure_ascii=False, indent=1))
|
||||
|
|
@ -31,6 +31,7 @@ from pathlib import Path
|
|||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import yaml # noqa: E402
|
||||
import underlay as U # noqa: E402
|
||||
import materiel as _materiel # noqa: E402
|
||||
from inventory_rules import expositions_des_applications # noqa: E402
|
||||
from inventory_rules import integrations_universelles # noqa: E402
|
||||
|
||||
|
|
@ -530,9 +531,10 @@ def inventaire() -> dict:
|
|||
# `serveur_prometheus_cibles_supplementaires` est le point d'extension prevu par le
|
||||
# role. On y met la fabric sous son PROPRE job : elle n'est pas la flotte, et un
|
||||
# tableau qui les melange ferait croire a un parc homogene.
|
||||
_fabric_metriques = sorted(
|
||||
f"{h['ip']}:9100" for h in U.hotes(u)
|
||||
if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11."))
|
||||
_fabric_nommes = {
|
||||
f"{h['ip']}:9100": str(h.get("nom")) for h in U.hotes(u)
|
||||
if str(h.get("role")) == "hyperviseur" and str(h.get("ip", "")).startswith("192.168.11.")}
|
||||
_fabric_metriques = sorted(_fabric_nommes)
|
||||
|
||||
# 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
|
||||
|
|
@ -819,9 +821,21 @@ def inventaire() -> dict:
|
|||
# une interface eteinte coupe une zone pendant que les autres vont bien.
|
||||
**({"serveur_icinga_fabric": _fabric_surveillee}
|
||||
if (_fabric_surveillee and "serveur_icinga" in _groupes_de(nom)) else {}),
|
||||
# LE MATERIEL : un catalogue, deux lecteurs (tableau Grafana, verdict Icinga).
|
||||
# Voir `scripts/materiel.py` — les seuils vivent la, une fois.
|
||||
**({"setops_materiel": _materiel.catalogue(),
|
||||
"setops_materiel_hotes": [
|
||||
{"nom": _n, "adresse": _c.rsplit(":", 1)[0], "job": "fabric",
|
||||
"role": "hyperviseur"}
|
||||
for _c, _n in sorted(_fabric_nommes.items(), key=lambda x: x[1])]}
|
||||
if (_fabric_nommes and ({"serveur_grafana", "serveur_icinga"}
|
||||
& set(_groupes_de(nom)))) else {}),
|
||||
# La fabric, en job separe — voir `_fabric_metriques` plus haut.
|
||||
**({"serveur_prometheus_cibles_supplementaires":
|
||||
[{"job": "fabric", "cibles": _fabric_metriques}]}
|
||||
# `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
|
||||
# souvenir quelle machine chauffe.
|
||||
[{"job": "fabric", "cibles": _fabric_metriques, "hotes": _fabric_nommes}]}
|
||||
if (_fabric_metriques and "serveur_prometheus" in _groupes_de(nom)) else {}),
|
||||
**communes,
|
||||
# En dernier : ce qu'une machine declare d'elle-meme prime sur ce que le
|
||||
|
|
|
|||
Loading…
Reference in a new issue