141 lines
5.9 KiB
Python
141 lines
5.9 KiB
Python
|
|
#!/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())
|