Set-OPS-Public/scripts/devis_versions.py

141 lines
5.9 KiB
Python
Raw Normal View History

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>
2026-08-10 23:02:16 -04:00
#!/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())