icinga : l'AC et l'historique survivent a la reconstruction ; temoins par cle

L'historique d'Icinga DB etait exclu de la restauration (2026-09-30) : son
environnement derive de l'AC d'Icinga, recreee a chaque reconstruction. L'AC est
desormais un jeu de sauvegarde, remis avant api setup ; la base d'Icinga est
restauree, et son schema n'est importe que sur une base vierge.

Les temoins comparaient PostgreSQL par nombre de lignes et n'ont pas vu 412
lignes d'historique remplacees par 380 neuves. Ils comparent desormais par cle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-10-07 15:18:40 -04:00
parent 12caff1cff
commit f04e790d2c
8 changed files with 231 additions and 41 deletions

View file

@ -1,5 +1,51 @@
# CHANGELOG — Set-OPS
## 2026-10-07 (103) — L'historique de supervision survit à la reconstruction ; les témoins comparent par clé
**Chezlepro reconstruite** sur `12caff1` (43 min, journal
`OPS-Chezlepro/logs/reconstruction-20261007-140824.log`) : rasage par la face (13/13), pool
`OPS-Chezlepro`, `valider` à 0 échec, pare-feu 13/13. Les 7 jeux restaurés depuis
l'instantané `avant-raser` de 14:08 — **toujours au dépôt** après le premier dépôt d'après
reconstruction : le défaut de M4 est corrigé dans les faits. Témoins : aucun écart.
**Ce que « 412 -> 380 lignes » cachait.** Comparées à l'instantané d'avant rasage, ligne à
ligne : les 412 lignes d'historique d'Icinga DB d'avant étaient **toutes** perdues, les 380
présentes toutes neuves (14:34 -> 14:49). Mesure par clé sur toute la base : ses 30 tables
renouvelées. Cause : une **décision du 2026-09-30** (`serveur_postgresql` excluait la base
d'Icinga de la restauration), parce qu'Icinga 2 dérive l'environnement d'Icinga DB — et tout
identifiant d'hôte, de service et d'historique — de **son AC**, recréée à chaque
reconstruction (`37503b53fd8…` avant, `0afe46c3…` après, mesuré). Restaurée seule, la base
serait restée orpheline.
**Décision revue avec l'exploitant** : l'historique se garde. Il porte la disponibilité
(`sla_history_state`) qu'une offre ou un label doit pouvoir prouver, et la mémoire des
incidents.
**Fait** :
- **Jeu `icinga`** au catalogue de `client_backup` : `/var/lib/icinga2/ca` et
`/var/lib/icinga2/icingadb.env`. `serveur_icinga` les remet **avant** `icinga2 api setup`,
qui réutilise une AC présente (« Found CA, skipping and using the existing one », mesuré
sur mon-01 ; aucun paquet n'en crée). `serveur_icinga` entre aux détenteurs d'état.
- `serveur_postgresql` ne refuse plus de restaurer la base d'Icinga (reste exclu :
`nextcloud`, qui se remet lui-même).
- L'import du schéma Icinga DB ne se fait plus que sur une base **vierge** : le marqueur vit
sur mon-01, que la reconstruction efface, alors que la base revient restaurée.
- **Les témoins comparent PostgreSQL par clé** (première colonne, au format du dump), et non
plus par nombre de lignes. Règles tirées de la mesure sur Chezlepro : **écart** pour une
base dont plus aucune ligne d'avant ne subsiste, et pour toute ligne perdue d'une table
d'historique ; listé sans écart, ce que Keycloak renouvelle de lui-même (`jgroups_ping`,
sessions, 23 configurations de composants sur 139). Jeu `icinga` : `ca.crt`, `ca.key`,
`icingadb.env` sont des identités. `template0` n'est plus dite « nouvelle ».
**Éprouvé** : `test_temoins.py`, 23 contrôles, dont le cas réel (même nombre de lignes,
aucune en commun : écart) et l'environnement d'Icinga changé (écart). Couverture : `serveur_icinga`
inclut son point de restauration. `make verifier` conforme, **94/94**.
**Reste** : déployer `client_backup` (mon-01 devient détenteur d'état : il déposera son AC
au prochain `sauvegarder`), rejouer les nouveaux témoins sur Chezlepro (ils doivent voir la
base d'Icinga non restaurée), puis reconstruire Technolibre : l'environnement doit garder
son identifiant, et l'historique d'avant rester visible dans la vigie.
## 2026-10-07 (102) — Le devis de placement vérifiait un gabarit que personne ne clone
**Trouvé au pré-vol de la reconstruction de Chezlepro** (`make placement-plan

View file

@ -148,9 +148,11 @@ elle, le premier dépôt de la machine reconstruite le chassait le jour même :
reconstruction de Technolibre (M4), il ne restait rien à quoi comparer l'état remis.
**Les témoins** comparent l'état vivant à cet instantané. Un écart, c'est une **perte**
(fichier, courriel, entrée de l'annuaire, base, rôle, table) ou une **identité changée** :
clés et certificats de l'AC, clé DKIM, `instanceid` de Nextcloud, mot de passe d'une entrée
de l'annuaire. Le reste (un courriel reçu depuis, une table de sessions, le bayes de rspamd,
(fichier, courriel, entrée de l'annuaire, base, rôle, table, ligne d'historique, ou une
base dont plus aucune ligne d'avant ne subsiste) ou une **identité changée** : clés et
certificats de l'AC, AC d'Icinga et environnement d'Icinga DB, clé DKIM, `instanceid` de
Nextcloud, mot de passe d'une entrée de l'annuaire. PostgreSQL se compare par clé (première
colonne de chaque table), pas par nombre de lignes. Le reste (un courriel reçu depuis, une table de sessions, le bayes de rspamd,
un cache) est listé sans être un écart. La reconstruction les lance à son étape `bilan`.
Les gestes :

View file

@ -83,6 +83,16 @@ client_backup_catalogue:
# PAS DE `serveur_web_frontal` (retire le 2026-09-30) : le frontal RELAIE, il ne sert
# rien lui-meme — ses vhosts, son WAF et sa page 404 se redeploient depuis le depot.
# `/srv/web` venait de l'epoque ou il servait du statique ; ses instantanes etaient vides.
serveur_icinga:
# L'AC D'ICINGA FAIT L'IDENTITE DE SA BASE (2026-10-07). Icinga 2 derive l'environnement
# d'Icinga DB de la cle publique de son AC, et chaque identifiant d'hote, de service et
# d'historique de cet environnement. Recreee a chaque reconstruction, elle rendait
# l'historique restaure orphelin : on l'avait donc jete (decision du 2026-09-30, revue
# le 2026-10-07 — l'historique, c'est la disponibilite qu'on doit pouvoir prouver).
nom: icinga
chemins:
- "{{ (serveur_icinga_ca | default('/var/lib/icinga2/ca/ca.crt')) | dirname }}"
- /var/lib/icinga2/icingadb.env
serveur_web_dorsal:
nom: web_dorsal
chemins: ["{{ serveur_web_dorsal_racine | default('/srv/webapp') }}"]

View file

@ -16,8 +16,10 @@ VERDICTS, PAR JEU :
CONFORME rien de perdu, l'identité est intacte ; des changements ordinaires, listés
(un courriel reçu depuis, une table de sessions, le bayes de rspamd) ;
ECART une PERTE (fichier, entrée, base, table), ou un objet d'IDENTITÉ changé :
clés et certificats de l'AC, clé DKIM, `instanceid` de Nextcloud, mot de
passe d'une entrée de l'annuaire. Exactement ce que la reconstruction du
clés et certificats de l'AC, AC d'Icinga et son environnement, clé DKIM,
`instanceid` de Nextcloud, mot de passe d'une entrée de l'annuaire ; une
base dont plus aucune ligne d'avant ne subsiste ; une ligne d'historique
perdue. Exactement ce que la reconstruction du
2026-09-30 avait perdu sans que rien ne le dise.
Code de sortie : 0 sans écart, 1 avec au moins un écart, 2 erreur d'usage.
@ -26,7 +28,9 @@ from __future__ import annotations
import argparse
import base64
import collections
import hashlib
import os
import re
import subprocess
import sys
@ -132,6 +136,12 @@ def jeu_fichiers(jeu: str, chemins: list[str], avant: Path) -> Rapport:
identite = lambda rel: rel.startswith(("certs/", "secrets/")) # noqa: E731
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=identite,
perte_stricte=identite)
elif jeu == "icinga":
# L'AC d'Icinga et l'environnement qu'elle derive : changes, chaque identifiant
# d'Icinga DB change, et tout l'historique restaure devient orphelin.
identite = lambda rel: rel in ("ca.crt", "ca.key", "icingadb.env") # noqa: E731
comparer_arbres(r, arbre(a_avant), arbre(a_vivant), strict=identite,
perte_stricte=identite)
elif jeu == "courriel":
comparer_arbres(r, arbre(a_avant, maildir), arbre(a_vivant, maildir),
strict=jamais, perte_stricte=est_message)
@ -218,58 +228,83 @@ def jeu_annuaire(avant_ldif: Path, vivant_ldif: Path) -> Rapport:
# --- PostgreSQL ----------------------------------------------------------------------
def lire_dump(texte: str) -> tuple[set[str], set[str], dict[tuple[str, str], int]]:
"""(bases, rôles, {(base, table) : lignes}) d'après un `pg_dumpall`."""
def lire_dump(texte: str) -> tuple[set[str], set[str], dict[tuple[str, str, str], list[str]]]:
"""(bases, rôles, {(base, table, 1re colonne) : [valeurs]}) d'après un `pg_dumpall`."""
bases = set(re.findall(r"^CREATE DATABASE (\S+) ", texte, re.M))
roles = set(re.findall(r"^CREATE ROLE (\S+);", texte, re.M))
lignes: dict[tuple[str, str], int] = {}
base, table, n = "", None, 0
cles: dict[tuple[str, str, str], list[str]] = {}
base, table = "", None
for ligne in texte.splitlines():
if table is not None:
if ligne == "\\.":
lignes[(base, table)] = n
table = None
else:
n += 1
cles[table].append(ligne.split("\t", 1)[0])
elif ligne.startswith("\\connect "):
base = ligne.split()[-1]
else:
m = re.match(r"^COPY (\S+) .*FROM stdin;$", ligne)
m = re.match(r"^COPY (\S+) \(([^,)]+)[,)].*FROM stdin;$", ligne)
if m:
table, n = m.group(1), 0
return bases, roles, lignes
table = (base, m.group(1), m.group(2).strip())
cles[table] = []
return bases, roles, cles
def psql(base: str, sql: str) -> list[str] | None:
r = subprocess.run(["runuser", "-u", "postgres", "--", "psql", "-tAX", "-d", base, "-c", sql],
capture_output=True, text=True)
capture_output=True, text=True, env=dict(os.environ, PAGER="cat"))
return r.stdout.splitlines() if r.returncode == 0 else None
def jeu_postgresql(dump: Path, interroger=psql) -> Rapport:
"""Par CLÉ, pas par nombre de lignes (2026-10-07).
Compter ne voyait rien : l'historique d'Icinga DB, entièrement remplacé (412 lignes
d'avant, toutes perdues ; 380 neuves), passait pour « 412 -> 380 ». On compare donc la
première colonne de chaque table, lue au même format que le dump (`COPY ... TO STDOUT`).
Ce qui est un ÉCART, d'après la mesure sur Chezlepro (avant-raser contre vivant) :
- une base dont AUCUNE ligne d'avant ne subsiste, toutes tables confondues : elle n'a
pas été restaurée (c'était icingadb, ses 30 tables renouvelées) ;
- une ligne perdue dans une table d'HISTORIQUE (`*history*`) : par nature, on y
ajoute, on n'y retire pas.
Le reste est listé : la découverte de grappe de Keycloak (`jgroups_ping`), ses sessions,
une configuration de composant régénérée renouvellent leurs clés dans la vie ordinaire.
"""
r = Rapport("postgresql")
if not dump.is_file():
r.ecarts.append(f"l'instantane n'emporte pas de dump ({dump.name})")
return r
bases, roles, lignes = lire_dump(dump.read_text(errors="replace"))
# TOUTES les bases, modeles compris : `pg_dumpall --clean` recree `template1`, et le
# premier essai reel (Technolibre, 2026-10-07) l'a dite « perdue » faute de la chercher.
bases, roles, cles = lire_dump(dump.read_text(errors="replace"))
vivantes = set(interroger("postgres", "select datname from pg_database") or [])
roles_vivants = set(interroger("postgres", "select rolname from pg_roles") or [])
r.compte = len(lignes)
for b in sorted(bases - vivantes):
r.ecarts.append(f"base perdue : {b}")
for x in sorted(roles - roles_vivants):
r.ecarts.append(f"role perdu : {x}")
for (b, t), n in sorted(lignes.items()):
if b not in vivantes:
par_base: dict[str, list[int]] = collections.defaultdict(lambda: [0, 0])
for (b, t, col), avant in sorted(cles.items()):
if b not in vivantes or not avant:
continue
compte = interroger(b, f"select count(*) from {t}")
if compte is None:
r.compte += 1
vivant = interroger(b, f"COPY (select {col} from {t}) TO STDOUT")
if vivant is None:
r.ecarts.append(f"table perdue : {b}.{t}")
elif int(compte[0]) != n:
r.changes.append(f"{b}.{t} : {n} -> {compte[0]} ligne(s)")
r.nouveaux.extend(f"base {b}" for b in sorted(vivantes - bases - {"postgres"}))
continue
a, v = set(avant), set(vivant)
communes = len(a & v)
par_base[b][0] += len(a)
par_base[b][1] += communes
perdues, nouvelles = len(a) - communes, len(v - a)
if perdues and "history" in t:
r.ecarts.append(f"historique perdu : {b}.{t} : {perdues} ligne(s) d'avant sur {len(a)}")
elif perdues or nouvelles:
r.changes.append(f"{b}.{t} : {perdues} retiree(s), {nouvelles} nouvelle(s) (avant {len(a)})")
for b, (n, communes) in sorted(par_base.items()):
if n and not communes:
r.ecarts.insert(0, f"base {b} : aucune des {n} ligne(s) d'avant ne subsiste — non restauree")
# `template0` n'est jamais dans un `pg_dumpall` : il n'est pas « nouveau », il est hors jeu.
r.nouveaux.extend(f"base {b}" for b in sorted(vivantes - bases - {"postgres", "template0"}))
return r

View file

@ -20,3 +20,4 @@ client_backup_groupes_etat:
- serveur_nextcloud
- serveur_rspamd
- serveur_web_dorsal
- serveur_icinga

View file

@ -155,6 +155,41 @@
# (2026-08-13), `hostname -f` rend le nom complet et Icinga s'emet spontanement un
# certificat CN et SAN = FQDN. Rien a forcer : il suffisait que la machine sache
# comment elle s'appelle.
# L'AC D'UNE INCARNATION PRECEDENTE REVIENT AVANT `api setup` (2026-10-07). Icinga 2 en
# derive l'environnement d'Icinga DB, donc l'identifiant de chaque hote, service et ligne
# d'historique. `api setup` la reutilise si elle est la (« Found CA, skipping and using the
# existing one », mesure sur mon-01) ; sinon il en fabrique une, et l'historique restaure
# par serveur_postgresql deviendrait orphelin. Aucun paquet n'en cree : c'est donc ICI.
- name: L'AC d'Icinga existe-t-elle deja ?
ansible.builtin.stat:
path: "{{ serveur_icinga_ca }}"
register: serveur_icinga_ca_presente
- name: Restauration — l'AC d'une incarnation precedente ?
ansible.builtin.include_role:
name: client_backup
tasks_from: restaurer.yml
vars:
client_backup_restaurer_jeu: serveur_icinga
client_backup_restaurer_vierge: "{{ not serveur_icinga_ca_presente.stat.exists }}"
- name: Restauration — remettre l'AC et l'environnement d'Icinga DB
changed_when: true
ansible.builtin.command:
argv: >-
{{ ['/usr/local/sbin/setops-restaurer', 'fichiers',
'--instantane', client_backup_restauration.instantane,
'--proprietaire', 'nagios:nagios'] + client_backup_restauration.chemins }}
when: client_backup_restauration.etat == 'a_restaurer'
- name: Restauration — acter l'AC remise
ansible.builtin.include_role:
name: client_backup
tasks_from: acter.yml
vars:
client_backup_acter_etat: restaure
when: client_backup_restauration.etat == 'a_restaurer'
- name: Configurer l'API Icinga 2
ansible.builtin.command:
cmd: icinga2 api setup
@ -174,12 +209,20 @@
enabled: true
state: started
- name: Importer le schema Icinga DB dans PostgreSQL (une fois)
# SUR UNE BASE VIERGE SEULEMENT (2026-10-07). Le marqueur vit sur mon-01 : une machine
# reconstruite ne l'a plus, alors que la base, elle, revient restauree par serveur_postgresql
# (historique compris). Rejouer le schema par-dessus y ajouterait une version et des erreurs
# « already exists » que psql taisait. La base dit elle-meme si elle a deja son schema.
- name: Importer le schema Icinga DB dans PostgreSQL (une fois, base vierge)
ansible.builtin.shell:
cmd: >-
PGPASSWORD='{{ serveur_icinga_db_password }}'
export PGPASSWORD='{{ serveur_icinga_db_password }}';
deja=$(psql -h {{ serveur_icinga_db_host }} -U {{ serveur_icinga_entree.proprietaire }}
-d {{ serveur_icinga_entree.base }} -tAX
-c "select to_regclass('public.icingadb_schema') is not null");
if [ "$deja" != t ]; then
psql -h {{ serveur_icinga_db_host }} -U {{ serveur_icinga_entree.proprietaire }}
-d {{ serveur_icinga_entree.base }} -f {{ serveur_icinga_schema }}
-d {{ serveur_icinga_entree.base }} -f {{ serveur_icinga_schema }}; fi
&& touch /etc/icingadb/.schema-imported
creates: /etc/icingadb/.schema-imported
no_log: true

View file

@ -80,11 +80,13 @@ serveur_postgresql_exportateur_mot_de_passe: "{{ vault_pg_exportateur | default(
# configuration vont ensemble, et le code de ses applications (user_oidc,
# richdocuments) n'est pas sauvegarde : restauree avant son installation, la base
# les dirait installees sans qu'elles le soient, et `occ app:install` refuserait ;
# - icinga : l'historique de supervision, lie a l'environnement d'Icinga, que rien ne
# sauvegarde. Il repart de zero, comme le reste de la supervision.
#
# PLUS `icinga` (2026-10-07). On l'excluait : l'historique de supervision etait lie a
# l'environnement d'Icinga, que l'AC d'Icinga, recreee a chaque reconstruction, renouvelait
# — restauree, la base serait restee orpheline. L'AC est desormais sauvegardee et remise
# (jeu `icinga` de client_backup) : l'environnement survit, et l'historique avec lui.
serveur_postgresql_restauration_consommateurs_exclus:
- nextcloud
- icinga
serveur_postgresql_bases_restaurables: >-
{{ ((serveur_postgresql_registre | default([]))

View file

@ -67,6 +67,7 @@ DUMP = r"""CREATE ROLE keycloak;
CREATE ROLE postgres;
CREATE DATABASE template1 WITH TEMPLATE = template0 ENCODING = 'UTF8';
CREATE DATABASE keycloak WITH TEMPLATE = template0 ENCODING = 'UTF8';
CREATE DATABASE icingadb WITH TEMPLATE = template0 ENCODING = 'UTF8';
\connect keycloak
COPY public.realm (id, name) FROM stdin;
1 maitre
@ -75,6 +76,14 @@ COPY public.realm (id, name) FROM stdin;
COPY public.user_session (id) FROM stdin;
a
\.
\connect icingadb
COPY public.host (id, name) FROM stdin;
\\x0a mon-01
\.
COPY public.history (id, event_time) FROM stdin;
\\x01 1
\\x02 2
\.
"""
@ -189,9 +198,12 @@ def postgresql() -> None:
ecrire(d / "avant/var/backups/setops/postgresql/toutes-bases.sql", DUMP)
argv = ["--avant", str(d / "avant"), "--instantane", "x", "--jeu", "postgresql=/var/backups/setops/postgresql",
"--dump", "/var/backups/setops/postgresql/toutes-bases.sql"]
AVANT = {"public.realm": ["1", "2"], "public.user_session": ["a"],
"public.host": ["\\\\x0a"], "public.history": ["\\\\x01", "\\\\x02"]}
def cluster(bases=("postgres", "template1", "keycloak"), roles=("postgres", "keycloak"), comptes=None):
comptes = comptes if comptes is not None else {"public.realm": "2", "public.user_session": "1"}
def cluster(bases=("postgres", "template0", "template1", "keycloak", "icingadb"),
roles=("postgres", "keycloak"), **tables):
contenu = {**AVANT, **tables}
def interroger(base: str, sql: str):
if "pg_database" in sql:
@ -200,30 +212,69 @@ def postgresql() -> None:
return [b for b in bases if not ("datistemplate" in sql and b == "template1")]
if "pg_roles" in sql:
return list(roles)
table = sql.rsplit(" ", 1)[-1]
return [comptes[table]] if table in comptes else None
table = sql.split(" from ", 1)[1].split(")", 1)[0]
return list(contenu[table]) if contenu.get(table) is not None else None
return interroger
rc, out = lancer(argv, cluster())
verifier(rc == 0 and "postgresql IDENTIQUE 2 table(s)" in out, f"memes bases, roles et lignes : IDENTIQUE ({out.strip()[-120:]})")
verifier(rc == 0 and "postgresql IDENTIQUE 4 table(s)" in out and "template0" not in out,
f"memes bases, roles et cles : IDENTIQUE ({out.strip()[-120:]})")
rc, out = lancer(argv, cluster(comptes={"public.realm": "2", "public.user_session": "7"}))
verifier(rc == 0 and "user_session : 1 -> 7" in out, "des sessions ouvertes depuis : dit, sans ecart")
rc, out = lancer(argv, cluster(**{"public.user_session": ["b", "c"]}))
verifier(rc == 0 and "public.user_session : 1 retiree(s), 2 nouvelle(s)" in out,
"des sessions renouvelees : dit, sans ecart (la vie ordinaire de Keycloak)")
rc, out = lancer(argv, cluster(bases=("postgres",)))
rc, out = lancer(argv, cluster(**{"public.history": ["\\\\x01", "\\\\x02", "\\\\x03"]}))
verifier(rc == 0 and "1 nouvelle(s)" in out, "un historique qui s'allonge : dit, sans ecart")
rc, out = lancer(argv, cluster(**{"public.history": ["\\\\x02", "\\\\x03"]}))
verifier(rc == 1 and "historique perdu : icingadb.public.history : 1 ligne(s)" in out,
"une ligne d'historique perdue est un ECART")
# Le cas reel du 2026-10-07 : meme nombre de lignes, aucune en commun. Compter ne le
# voyait pas ; c'etait la base d'Icinga, non restauree.
rc, out = lancer(argv, cluster(**{"public.host": ["\\\\xff"], "public.history": ["\\\\x08", "\\\\x09"]}))
verifier(rc == 1 and "base icingadb : aucune des 3 ligne(s) d'avant ne subsiste" in out,
"une base entierement renouvelee, a nombre de lignes egal, est un ECART")
rc, out = lancer(argv, cluster(bases=("postgres", "template1", "icingadb")))
verifier(rc == 1 and "base perdue : keycloak" in out, "une base perdue est un ECART")
rc, out = lancer(argv, cluster(roles=("postgres",)))
verifier(rc == 1 and "role perdu : keycloak" in out, "un role perdu est un ECART")
rc, out = lancer(argv, cluster(comptes={"public.user_session": "1"}))
rc, out = lancer(argv, cluster(**{"public.realm": None}))
verifier(rc == 1 and "table perdue : keycloak.public.realm" in out, "une table perdue est un ECART")
def icinga() -> None:
with tempfile.TemporaryDirectory() as d:
d = Path(d)
vivant = d / "vivant"
for racine in (d / "avant" / str(vivant).lstrip("/"), vivant):
ecrire(racine / "icinga2/ca/ca.crt", "AC")
ecrire(racine / "icinga2/ca/ca.key", "CLE")
ecrire(racine / "icinga2/icingadb.env", '"37503b53fd8"')
ecrire(vivant / "icinga2/ca/serial.txt", "02")
argv = ["--avant", str(d / "avant"), "--instantane", "x",
"--jeu", f"icinga={vivant}/icinga2/ca:{vivant}/icinga2/icingadb.env"]
rc, out = lancer(argv)
verifier(rc == 0, f"l'AC d'Icinga remise, un certificat de noeud signe depuis : sans ecart ({out.strip()[-120:]})")
ecrire(vivant / "icinga2/icingadb.env", '"0afe46c3"')
rc, out = lancer(argv)
verifier(rc == 1 and "IDENTITE icingadb.env" in out,
"un environnement d'Icinga DB change est un ECART (l'historique deviendrait orphelin)")
ecrire(vivant / "icinga2/icingadb.env", '"37503b53fd8"')
ecrire(vivant / "icinga2/ca/ca.key", "AUTRE CLE")
rc, out = lancer(argv)
verifier(rc == 1 and "IDENTITE ca.key" in out, "une cle d'AC d'Icinga changee est un ECART")
def main() -> int:
fichiers()
annuaire()
postgresql()
icinga()
if ECHECS:
print(f"\n{len(ECHECS)} echec(s).")
return 1