Some checks are pending
verifier / verifier (push) Waiting to run
Trois depots tiers etaient en HTTPS, et client_artefacts pose Acquire::https::Proxy DIRECT — sans quoi le cache du site refuse les tunnels et aucun depot tiers n est joignable. La ligne est juste ; sa consequence ne l avait pas ete vue : ces trois depots CONTOURNENT le cache, et chaque VM neuve allait les chercher sur Internet a sa naissance. step-cli et alloy sont poses par des integrations UNIVERSELLES. Sans lien, une machine neuve n obtenait ni son client d autorite ni ses metriques — elle n entrait dans aucun flux chiffre. Dernier obstacle a une reconstruction hors ligne, et il tenait dans un mot. make cacher-paquets tire les 21 deb aux versions epinglees, empreinte SHA256 verifiee depuis l index du depot. Le role partage paquets_tiers les depose et les installe EN UN SEUL appel a apt — il sait resoudre un ensemble de fichiers locaux qui se dependent, la ou paquet par paquet echouerait sur l ordre (Icinga en apporte dix-sept). Six roles branches ; chacun retombe sur le depot distant pour ce que le cache n a PAS fourni, et rien d autre. Eprouve pour de vrai : packages.smallstep.com renvoye vers 127.0.0.1, step-cli desinstalle, cache local efface. Le role rejoue installe 0.30.6-1 depuis le cache et la machine obtient ses certificats. Zero echec. Deux defauts de mon propre outil, trouves en le construisant : Smallstep sert son index NON COMPRESSE (404 sur .gz) donc step-cli n etait jamais mis en cache ; et un depot injoignable faisait continue AVANT d incrementer le total, si bien que le script rapportait 20 sur 20 alors qu il en manquait un. Une garde qui ne peut pas echouer ne garde rien — troisieme fois cette semaine. Corrige puis eprouve en le faisant echouer. Reste hors ligne : NTP externe, et l expedition des alertes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019f91zs9SDdwSzL2CVei6on
165 lines
7.1 KiB
Python
165 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Tire les paquets NON DEBIAN dans le cache du controleur, une fois, verifies.
|
|
|
|
POURQUOI CE SCRIPT EXISTE (mesure du 2026-09-03).
|
|
|
|
Trois depots tiers sont configures sur la flotte — Smallstep, Icinga, Grafana — tous en
|
|
HTTPS. Or `client_artefacts` pose `Acquire::https::Proxy "DIRECT"`, sans quoi le cache
|
|
refuse les tunnels HTTPS et aucun depot tiers n'est joignable. La consequence n'avait pas
|
|
ete vue : ces trois depots CONTOURNENT LE CACHE, et chaque VM neuve va les chercher sur
|
|
Internet a sa naissance.
|
|
|
|
`step-cli` (Smallstep) et `alloy` (Grafana) sont poses par des integrations UNIVERSELLES.
|
|
Sans Internet, une machine neuve n'obtient donc ni son client d'autorite ni ses metriques
|
|
— elle ne peut entrer dans aucun flux chiffre. C'etait le dernier obstacle a une
|
|
reconstruction hors ligne.
|
|
|
|
MEME PATRON QUE LE RESTE DU CACHE DU CONTROLEUR (Forgejo, Keycloak, Nextcloud,
|
|
oauth2-proxy) : on telecharge UNE FOIS, on verifie, on depose ensuite depuis le cache.
|
|
|
|
CE QU'IL NE FAIT PAS, ET POURQUOI. Il ne resout aucune dependance. Les dependances de ces
|
|
paquets sont des paquets DEBIAN, qui passent deja par le cache du site en HTTP — c'est
|
|
justement la difference qu'on corrige ici. Ce script ne s'occupe que de ce que Debian ne
|
|
fournit pas.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import gzip
|
|
import hashlib
|
|
import sys
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
import yaml
|
|
|
|
RACINE = Path(__file__).resolve().parent.parent
|
|
DECLARATION = RACINE / "paquets-tiers.yml"
|
|
CACHE = Path.home() / ".cache" / "setops" / "paquets"
|
|
|
|
|
|
def _index(uri: str, suite: str, composant: str, arch: str = "amd64") -> dict[str, dict]:
|
|
"""Les paquets d'un depot, lus dans SON index — pas devines depuis une URL.
|
|
|
|
Le chemin d'un `.deb` dans un depot n'est pas derivable de son nom : il vit dans le
|
|
champ `Filename` de l'index. Le construire a la main marcherait pour un depot et
|
|
casserait au suivant. On lit donc l'index, qui porte aussi le `SHA256` — la
|
|
verification vient avec, sans effort supplementaire.
|
|
"""
|
|
# TOUS LES DEPOTS NE COMPRESSENT PAS LEUR INDEX (mesure du 2026-09-03).
|
|
#
|
|
# Icinga et Grafana servent `Packages.gz` ; Smallstep sert `Packages` en clair, et
|
|
# rend 404 sur les autres formes. La premiere version de ce script ne demandait que
|
|
# `.gz` : le depot Smallstep echouait en silence, et `step-cli` — le paquet de CHAQUE
|
|
# machine — n'a jamais ete mis en cache. On essaie donc les formes courantes, dans
|
|
# l'ordre du moins couteux au plus.
|
|
base = f"{uri.rstrip('/')}/dists/{suite}/{composant}/binary-{arch}"
|
|
brut = None
|
|
derniere = None
|
|
for suffixe in (".gz", "", ".xz"):
|
|
try:
|
|
with urllib.request.urlopen(base + "/Packages" + suffixe, timeout=60) as rep:
|
|
donnees = rep.read()
|
|
except Exception as e: # noqa: BLE001 — on essaie la forme suivante
|
|
derniere = e
|
|
continue
|
|
if suffixe == ".gz":
|
|
donnees = gzip.decompress(donnees)
|
|
elif suffixe == ".xz":
|
|
import lzma
|
|
donnees = lzma.decompress(donnees)
|
|
brut = donnees.decode("utf-8", "replace")
|
|
break
|
|
if brut is None:
|
|
raise derniere or RuntimeError(f"aucun index lisible sous {base}")
|
|
paquets: dict[str, dict] = {}
|
|
for bloc in brut.split("\n\n"):
|
|
champs: dict[str, str] = {}
|
|
cle = None
|
|
for ligne in bloc.splitlines():
|
|
if ligne.startswith((" ", "\t")) and cle:
|
|
continue
|
|
if ":" in ligne:
|
|
cle, _, val = ligne.partition(":")
|
|
champs[cle.strip()] = val.strip()
|
|
nom, ver = champs.get("Package"), champs.get("Version")
|
|
if nom and ver:
|
|
paquets[f"{nom}={ver}"] = champs
|
|
return paquets
|
|
|
|
|
|
def _telecharger(uri: str, champs: dict, dest: Path) -> str:
|
|
url = f"{uri.rstrip('/')}/{champs['Filename'].lstrip('/')}"
|
|
with urllib.request.urlopen(url, timeout=300) as rep:
|
|
contenu = rep.read()
|
|
somme = hashlib.sha256(contenu).hexdigest()
|
|
attendue = champs.get("SHA256", "")
|
|
if attendue and somme != attendue:
|
|
raise SystemExit(
|
|
f"ERREUR: empreinte refusee pour {dest.name}\n"
|
|
f" attendue {attendue}\n obtenue {somme}\n"
|
|
" Le fichier n'est pas celui que l'index annonce — on ne le garde pas.")
|
|
dest.write_bytes(contenu)
|
|
return somme
|
|
|
|
|
|
def main() -> int:
|
|
if not DECLARATION.is_file():
|
|
print(f"ERREUR: {DECLARATION.name} absent — rien de declare a mettre en cache.",
|
|
file=sys.stderr)
|
|
return 2
|
|
decl = yaml.safe_load(DECLARATION.read_text(encoding="utf-8")) or {}
|
|
CACHE.mkdir(parents=True, exist_ok=True)
|
|
|
|
total = tires = deja = 0
|
|
manifeste: list[str] = []
|
|
for nom_depot, d in sorted((decl.get("depots") or {}).items()):
|
|
voulus = d.get("paquets") or {}
|
|
if not voulus:
|
|
continue
|
|
print(f" {nom_depot} : lecture de l'index…")
|
|
try:
|
|
index = _index(d["uri"], d["suite"], d["composant"])
|
|
except Exception as e:
|
|
# ON COMPTE QUAND MEME CE QU'IL DEVAIT FOURNIR (2026-09-03).
|
|
#
|
|
# La premiere version faisait `continue` AVANT d'incrementer `total` : un
|
|
# depot injoignable disparaissait donc du decompte, et le script concluait
|
|
# « 20 tires, 20 declares » alors qu'il en manquait un — le plus important.
|
|
# Une garde qui ne peut pas echouer ne garde rien.
|
|
total += len(voulus)
|
|
print(f" ! {nom_depot} injoignable ({type(e).__name__}: {str(e)[:60]}) — "
|
|
f"{len(voulus)} paquet(s) NON mis en cache.", file=sys.stderr)
|
|
continue
|
|
for paquet, version in sorted(voulus.items()):
|
|
total += 1
|
|
cle = f"{paquet}={version}"
|
|
champs = index.get(cle)
|
|
if champs is None:
|
|
# DIRE CE QU'ON NE TROUVE PAS. Un amont qui retire une version epinglee
|
|
# est un fait a connaitre AVANT la prochaine reconstruction, pas pendant.
|
|
print(f" ! {cle} absent de l'index de {nom_depot} — version retiree en "
|
|
"amont ? Ajuster `paquets-tiers.yml`.", file=sys.stderr)
|
|
continue
|
|
fichier = CACHE / Path(champs["Filename"]).name
|
|
if fichier.is_file() and champs.get("SHA256") == hashlib.sha256(
|
|
fichier.read_bytes()).hexdigest():
|
|
deja += 1
|
|
manifeste.append(f"{cle} {fichier.name}")
|
|
continue
|
|
somme = _telecharger(d["uri"], champs, fichier)
|
|
tires += 1
|
|
manifeste.append(f"{cle} {fichier.name}")
|
|
print(f" + {fichier.name} ({len(fichier.read_bytes()) // 1024} Ko, "
|
|
f"sha256 {somme[:12]}…)")
|
|
|
|
(CACHE / "MANIFESTE").write_text("\n".join(sorted(manifeste)) + "\n", encoding="utf-8")
|
|
print(f"\n {tires} tire(s), {deja} deja en cache, {total} declare(s).")
|
|
print(f" Cache : {CACHE}")
|
|
if tires + deja < total:
|
|
print("\n INCOMPLET — la reconstruction hors ligne echouera sur ce qui manque.")
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|