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:
parent
12caff1cff
commit
f04e790d2c
8 changed files with 231 additions and 41 deletions
46
CHANGELOG.md
46
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 :
|
||||
|
|
|
|||
|
|
@ -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') }}"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,3 +20,4 @@ client_backup_groupes_etat:
|
|||
- serveur_nextcloud
|
||||
- serveur_rspamd
|
||||
- serveur_web_dorsal
|
||||
- serveur_icinga
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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([]))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue