166 lines
7.1 KiB
Python
166 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())
|