devis : make versions-mesurer — l'ecart avec l'amont devient lisible
Question de l'exploitant : « ne devrait-on pas prendre les versions les plus recentes ? » Non — resoudre « la derniere » au deploiement detruirait la reproductibilite qu'on vient de prouver en rasant et remontant deux ecosystemes, et ferait de chaque reconstruction une loterie. Six majeures de Forgejo ne s'avalent pas en effet de bord d'un `make`. Mais il avait raison de tirer le fil : L'ECART ETAIT INVISIBLE. Il a fallu quatre requetes a la main pour decouvrir Forgejo 10.0.0 contre v16.0.2 publie, Keycloak 26.0.7 contre 26.7.1, Nextcloud 34.0.1 contre 34.0.2, oauth2-proxy a jour. Le devis ne juge pas, il RENSEIGNE : un retard n'est pas une faute, il sort donc en 0. Monter de version reste un acte explicite. Ce qui le distingue d'une liste ecrite a la main : les versions epinglees sont DERIVEES du depot. La source amont, elle, ne peut pas se deriver — elle depend de l'editeur — et vit dans une table. TOUTE VERSION EPINGLEE SANS ENTREE DANS CETTE TABLE FAIT SORTIR EN ERREUR : sans cette garde, un epinglage ajoute demain vieillirait sans que personne ne le voie. Une exemption reste possible, mais ecrite et motivee (deux le sont : la serie PHP de Debian, la version PostgreSQL vide). Eprouve dans les deux sens : un serveur_redis_version ajoute temporairement fait sortir en 1 en le nommant ; retire, retour a 0. Et il rappelle ce qu'on n'epingle PAS. Grafana n'a aucune version dans le depot — le 13.1.1 vu dans l'historique apt etait ce que son depot servait ce jour-la. Debian, Grafana, smallstep et Icinga prennent ce qu'on leur donne. Deux regimes coexistent dans la meme flotte, et les taire donnerait l'illusion que tout est maitrise. Verifie : versions-mesurer code 0, garde eprouvee, prouver.py 35 OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
a1908f2c63
commit
a86a82b7ae
4 changed files with 207 additions and 1 deletions
40
CHANGELOG.md
40
CHANGELOG.md
|
|
@ -1,5 +1,45 @@
|
|||
# CHANGELOG — Set-OPS
|
||||
|
||||
## 2026-08-10 — `make versions-mesurer` : l'écart avec l'amont devient lisible
|
||||
|
||||
Question de l'exploitant : *« ne devrait-on pas prendre les versions les plus récentes ? »*
|
||||
|
||||
**Non**, et la réponse tient en trois points. Résoudre « la dernière » au moment du
|
||||
déploiement **détruirait la reproductibilité** — celle-là même qu'on vient de prouver en
|
||||
rasant et remontant deux écosystèmes. Ça transformerait chaque déploiement en **loterie** :
|
||||
une heure de reconstruction ne doit pas dépendre de ce qu'un tiers a publié cette nuit. Et
|
||||
six versions majeures de Forgejo ne s'avalent pas en effet de bord d'un `make` — ça se fait
|
||||
délibérément, avec une sauvegarde avant et une vérification après.
|
||||
|
||||
**Mais le vrai problème était ailleurs, et l'exploitant avait raison de tirer le fil :
|
||||
l'écart était invisible.** Il a fallu quatre requêtes à la main pour découvrir l'état réel :
|
||||
|
||||
| Composant | Épinglé | Publié |
|
||||
|---|---|---|
|
||||
| oauth2-proxy | `v7.15.3` | **à jour** |
|
||||
| Nextcloud | `34.0.1` | `34.0.2` |
|
||||
| Keycloak | `26.0.7` | `26.7.1` |
|
||||
| Forgejo | `10.0.0` | **`v16.0.2`** |
|
||||
|
||||
Le devis ne juge pas, il **renseigne**. Un retard n'est pas une faute ; il sort donc en 0.
|
||||
|
||||
**Ce qui le distingue d'une liste écrite à la main** : les versions épinglées sont *dérivées*
|
||||
du dépôt (`roles/*/defaults/*_version`). La source amont, elle, ne peut pas se dériver —
|
||||
elle dépend de l'éditeur — et vit dans une table. **Toute version épinglée sans entrée dans
|
||||
cette table fait sortir en erreur.** Sans cette garde, un épinglage ajouté demain vieillirait
|
||||
sans que personne ne le voie, et on croirait tout surveillé alors qu'on ne verrait plus rien.
|
||||
Une exemption reste possible, mais **écrite et motivée** — deux le sont déjà (la série PHP de
|
||||
Debian, la version PostgreSQL vide par défaut).
|
||||
|
||||
Éprouvé dans les deux sens : un `serveur_redis_version` ajouté temporairement fait sortir en
|
||||
**1** en le nommant ; retiré, retour à **0**.
|
||||
|
||||
**Et il rappelle ce qu'on n'épingle pas.** Grafana n'a aucune version dans le dépôt — le
|
||||
`13.1.1` vu dans l'historique `apt` était simplement ce que le dépôt Grafana servait ce
|
||||
jour-là. Debian, Grafana, smallstep et Icinga prennent tous ce qu'on leur donne au moment du
|
||||
déploiement. **Deux régimes coexistent dans la même flotte**, et les taire donnerait
|
||||
l'illusion que tout est maîtrisé.
|
||||
|
||||
## 2026-08-10 — `raser` n'annonce plus des destructions qui n'ont pas eu lieu
|
||||
|
||||
Le défaut noté la veille est corrigé. `raser` lisait l'**accusé de réception** de l'API et
|
||||
|
|
|
|||
5
Makefile
5
Makefile
|
|
@ -419,7 +419,7 @@ ca-empreinte: ansible-runtime _instance-requise ## Empreinte de la racine, lue S
|
|||
-a "step certificate fingerprint /etc/step-ca/certs/root_ca.crt" 2>/dev/null \
|
||||
| tail -1 | tr -d ' \r'
|
||||
|
||||
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer
|
||||
.PHONY: frontiere-plan frontiere-appliquer frontiere-mesurer mtu-mesurer versions-mesurer
|
||||
courriel-plan: ansible-runtime ## Chaine Postfix -> LDAP -> Dovecot -> IMAP (aucune ecriture)
|
||||
@rm -f $(SETOPS_INSTANCE)/devis-courriel.json.*
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-courriel.yml >/dev/null
|
||||
|
|
@ -434,6 +434,9 @@ expositions-plan: ansible-runtime ## Chaque exposition du plan repond-elle ? (ed
|
|||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-expositions.yml >/dev/null
|
||||
@python3 scripts/devis_expositions.py
|
||||
|
||||
versions-mesurer: ## De combien nos epinglages ont-ils vieilli ? (interroge les amonts, aucune ecriture)
|
||||
@python3 scripts/devis_versions.py
|
||||
|
||||
mtu-mesurer: ansible-runtime ## L'invite porte-t-il le MTU de sa zone SDN ? (aucune ecriture)
|
||||
@ansible-playbook -i $(SETOPS_INVENTAIRE) playbooks/maintenance/devis-mtu.yml >/dev/null
|
||||
@python3 scripts/devis_mtu.py
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ make postgresql-plan # chiffrement imposé, et à quels réseaux
|
|||
make courriel-plan # Postfix → LDAP → LMTP → Dovecot → IMAP
|
||||
make frontiere-mesurer # ce qui n'est pas déclaré à la frontière est-il refusé ?
|
||||
make mtu-mesurer # l'invité porte-t-il le MTU de sa zone SDN ?
|
||||
make versions-mesurer # de combien nos épinglages ont-ils vieilli ?
|
||||
```
|
||||
|
||||
## Le trou qu'il comble
|
||||
|
|
@ -69,6 +70,28 @@ elle-même, le certificat était **expiré depuis plus de huit heures** et le re
|
|||
échouait toutes les quatorze minutes sur `'step ca renew' requires the '--ca-url' flag`.
|
||||
Rien ne le signalait.
|
||||
|
||||
## Le devis des versions
|
||||
|
||||
Il répond à une question posée le 2026-08-10 : *« ne devrait-on pas prendre les versions les
|
||||
plus récentes ? »* Non — résoudre « la dernière » au moment du déploiement détruirait la
|
||||
reproductibilité et ferait de chaque reconstruction une loterie. Mais **l'écart, lui, était
|
||||
invisible** : il a fallu quatre requêtes à la main pour découvrir que Forgejo avait six
|
||||
versions majeures de retard.
|
||||
|
||||
Ce devis ne juge pas, il **renseigne**. Un retard n'est pas une faute ; monter de version
|
||||
reste un acte explicite — on édite, on relit, on reconstruit, on repasse les devis.
|
||||
|
||||
La liste des versions épinglées est **dérivée** du dépôt (`roles/*/defaults/*_version`). La
|
||||
source amont de chacune, elle, ne peut pas se dériver — elle dépend de l'éditeur — et vit
|
||||
donc dans une table. **Toute version épinglée sans entrée dans cette table fait sortir le
|
||||
devis en erreur** : sans cette garde, un épinglage ajouté demain vieillirait sans que
|
||||
personne ne le voie, et on croirait tout surveillé. Une exemption reste possible, mais
|
||||
**écrite et motivée**.
|
||||
|
||||
Il rappelle enfin ce que l'on n'épingle pas : les paquets Debian, Grafana, smallstep et
|
||||
Icinga prennent ce que leur dépôt sert au moment du déploiement. Deux régimes coexistent, et
|
||||
les taire donnerait l'illusion que tout est maîtrisé.
|
||||
|
||||
## Le devis d'étanchéité de la frontière
|
||||
|
||||
Le devis des expositions pose la question **positive** — chaque exposition déclarée
|
||||
|
|
|
|||
140
scripts/devis_versions.py
Normal file
140
scripts/devis_versions.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Devis des versions : de combien nos epinglages ont-ils vieilli ?
|
||||
|
||||
L'exploitant, le 2026-08-10 : « ne devrait-on pas prendre les versions les plus recentes ? »
|
||||
La reponse est non — resoudre « la derniere » au moment du deploiement detruirait la
|
||||
reproductibilite, et transformerait chaque reconstruction en loterie. Mais l'ECART, lui,
|
||||
etait invisible : il a fallu quatre requetes a la main pour decouvrir que Forgejo avait six
|
||||
versions majeures de retard.
|
||||
|
||||
C'est ca que ce devis corrige. Il ne juge pas, il RENSEIGNE — un retard n'est pas une faute,
|
||||
c'est une information qu'on doit pouvoir lire en dix secondes.
|
||||
|
||||
CE QU'IL FAIT, et ce qui le distingue d'une liste ecrite a la main :
|
||||
|
||||
- la liste des versions epinglees est DERIVEE du depot (`roles/*/defaults/*_version`) ;
|
||||
- la source amont de chacune est declaree ici, dans SOURCES — la seule chose qui ne peut
|
||||
pas se deriver, puisqu'elle depend de l'editeur ;
|
||||
- **toute version epinglee sans entree dans SOURCES fait sortir en erreur.** Sans cette
|
||||
garde, ajouter un epinglage demain le rendrait invisible a ce devis, et on croirait tout
|
||||
surveille alors qu'on ne verrait plus rien. Une exemption reste possible, mais elle doit
|
||||
etre ECRITE et motivee.
|
||||
|
||||
CE QU'IL NE FAIT PAS : monter de version. C'est un acte explicite — on edite, on relit, on
|
||||
reconstruit, on repasse les devis.
|
||||
|
||||
make versions-mesurer
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
RACINE = Path(__file__).resolve().parent.parent
|
||||
DELAI = 20
|
||||
|
||||
# Ou demander « la derniere version publiee ». `type` dit comment lire la reponse.
|
||||
# Une entree `exempt` porte la RAISON de ne pas verifier — jamais un simple silence.
|
||||
SOURCES: dict[str, dict] = {
|
||||
"serveur_keycloak_version": {
|
||||
"type": "github", "depot": "keycloak/keycloak"},
|
||||
"serveur_oauth2_proxy_version": {
|
||||
"type": "github", "depot": "oauth2-proxy/oauth2-proxy"},
|
||||
"serveur_forgejo_version": {
|
||||
"type": "codeberg", "depot": "forgejo/forgejo"},
|
||||
"serveur_nextcloud_version": {
|
||||
"type": "nextcloud"},
|
||||
"serveur_nextcloud_php_version": {
|
||||
"exempt": "serie de paquets Debian : elle suit la distribution, pas un editeur"},
|
||||
"serveur_postgresql_version": {
|
||||
"exempt": "vide par defaut — la version vient du paquet Debian"},
|
||||
}
|
||||
|
||||
|
||||
def _http(url: str, entete: dict | None = None) -> str | None:
|
||||
req = urllib.request.Request(url, headers=entete or {"User-Agent": "setops-devis"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=DELAI) as r:
|
||||
return r.read().decode("utf-8", "replace")
|
||||
except (urllib.error.URLError, OSError, TimeoutError):
|
||||
return None
|
||||
|
||||
|
||||
def _amont(src: dict) -> str | None:
|
||||
"""La derniere version publiee, ou None si l'amont n'a pas repondu."""
|
||||
t = src.get("type")
|
||||
if t == "github":
|
||||
c = _http(f"https://api.github.com/repos/{src['depot']}/releases/latest")
|
||||
return (json.loads(c).get("tag_name") if c else None)
|
||||
if t == "codeberg":
|
||||
c = _http(f"https://codeberg.org/api/v1/repos/{src['depot']}/releases?limit=1")
|
||||
d = json.loads(c) if c else []
|
||||
return (d[0].get("tag_name") if d else None)
|
||||
if t == "nextcloud":
|
||||
c = _http("https://download.nextcloud.com/server/releases/")
|
||||
if not c:
|
||||
return None
|
||||
v = re.findall(r"nextcloud-(\d+\.\d+\.\d+)\.tar\.bz2", c)
|
||||
return max(v, key=lambda x: [int(n) for n in x.split(".")]) if v else None
|
||||
return None
|
||||
|
||||
|
||||
def epinglees() -> dict[str, tuple[str, str]]:
|
||||
"""{variable: (valeur, role)} — derive du depot, jamais saisi."""
|
||||
trouve: dict[str, tuple[str, str]] = {}
|
||||
for f in sorted((RACINE / "roles").glob("*/defaults/main.yml")):
|
||||
for ligne in f.read_text(encoding="utf-8", errors="ignore").splitlines():
|
||||
m = re.match(r'^([a-z0-9_]+_version):\s*"?([^"#\n]*)"?\s*$', ligne)
|
||||
if m and m.group(1) not in trouve:
|
||||
trouve[m.group(1)] = (m.group(2).strip(), f.parent.parent.name)
|
||||
return trouve
|
||||
|
||||
|
||||
def main() -> int:
|
||||
pins = epinglees()
|
||||
print(f"Devis des versions — {len(pins)} epinglage(s) trouve(s) dans les roles\n")
|
||||
|
||||
inconnues = [v for v in pins if v not in SOURCES]
|
||||
lignes, retards, muets = [], 0, 0
|
||||
for var, (val, role) in sorted(pins.items()):
|
||||
src = SOURCES.get(var)
|
||||
if src is None:
|
||||
continue
|
||||
if "exempt" in src:
|
||||
lignes.append(f" -- {role:<22} {val or '(vide)':<12} exempte : {src['exempt']}")
|
||||
continue
|
||||
derniere = _amont(src)
|
||||
if derniere is None:
|
||||
lignes.append(f" ? {role:<22} {val:<12} l'amont n'a pas repondu")
|
||||
muets += 1
|
||||
elif derniere.lstrip("v") == val.lstrip("v"):
|
||||
lignes.append(f" ok {role:<22} {val:<12} a jour")
|
||||
else:
|
||||
lignes.append(f" RETARD {role:<21} {val:<12} -> {derniere} publie")
|
||||
retards += 1
|
||||
print("\n".join(lignes))
|
||||
|
||||
print(f"\n {retards} en retard, {muets} sans reponse de l'amont.")
|
||||
print(" Un retard n'est pas une faute : monter de version est un acte explicite —\n"
|
||||
" on edite, on relit, on reconstruit, on repasse les devis.")
|
||||
|
||||
# Ce qui n'est PAS epingle merite d'etre dit : le depot melange deux regimes, et
|
||||
# l'ignorer donnerait l'illusion que tout est maitrise.
|
||||
print("\n Rappel : seuls ces artefacts sont epingles. Les paquets (Debian, Grafana,\n"
|
||||
" smallstep, Icinga) prennent ce que leur depot sert au moment du deploiement.")
|
||||
|
||||
if inconnues:
|
||||
print(f"\nNON COUVERT : {len(inconnues)} version(s) epinglee(s) qu'aucune source ne "
|
||||
f"surveille :\n - " + "\n - ".join(sorted(inconnues))
|
||||
+ "\n Ajouter une entree dans SOURCES (ou une exemption motivee) : sans cela,\n"
|
||||
" cet epinglage vieillirait sans que personne ne le voie.")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Reference in a new issue