VMs of one host pulled the same packages again and again, and an install could not be proven to work without the network. A Go service intercepts the bridge transparently: a package file is served from disk, an index is always revalidated upstream and replayed only when upstream is mute, git is mirrored. Deployment poses the cache authority in QEMU and Proxmox VE guests, and can cut the network for a whole install. The TODO menu installs, diagnoses, fills, cleans and copies the store; long_test/qemu_cache.py measures that a second VM pulls no package upstream. Covered by Go tests and test/test_qemu_cache_*.py. --- FR --- Les VM d'un hôte tiraient sans cesse les mêmes paquets, et rien ne prouvait qu'une installation marche sans réseau. Un service Go intercepte le pont de façon transparente : un fichier de paquet est servi du disque, un index est toujours revalidé à l'amont et rejoué seulement quand l'amont est muet, git est mis en miroir. Le déploiement pose l'autorité du cache dans les invités QEMU et Proxmox VE, et peut couper le réseau pour toute une installation. Le menu TODO installe, diagnostique, remplit, nettoie et emporte le magasin ; long_test/qemu_cache.py mesure qu'une seconde VM ne tire aucun paquet de l'amont. Couvert par les tests Go et test/test_qemu_cache_*.py. Assisted-by: Claude Opus 5
92 lines
3.2 KiB
Python
92 lines
3.2 KiB
Python
#!/usr/bin/env python3
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
"""Le journal d'accès du cache, rendu lisible ligne à ligne.
|
|
|
|
Lit les lignes JSON du journal sur l'entrée standard et en écrit une par
|
|
requête servie. La flèche est la colonne qui compte sous une coupure : « ↑ »
|
|
dit que la requête est SORTIE vers l'internet, « · » qu'elle a été servie du
|
|
disque de l'hôte. Un déploiement hors ligne n'en produit aucune en « ↑ », et
|
|
« --amont » ne garde que celles-là : la commande reste alors muette tant que
|
|
rien ne sort.
|
|
|
|
Chaque ligne est écrite dès qu'elle est lue. « tail -f » y passe un flux sans
|
|
fin, et une sortie mise en tampon ne montrerait rien avant plusieurs
|
|
kilo-octets, c'est-à-dire des minutes sur un journal qui avance doucement.
|
|
|
|
Une ligne illisible est passée : le journal s'écrit pendant qu'on le lit, et
|
|
la dernière ligne d'un fichier en cours d'écriture est parfois tronquée.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
|
|
|
|
def taille(octets):
|
|
"""Une taille lisible, « - » quand la réponse n'avait pas de corps."""
|
|
n = float(octets or 0)
|
|
if not n:
|
|
return "-"
|
|
for unite in ("o", "Kio", "Mio", "Gio"):
|
|
if n < 1024 or unite == "Gio":
|
|
return f"{n:.0f} {unite}" if unite == "o" else f"{n:.1f} {unite}"
|
|
n /= 1024
|
|
return f"{n:.1f} Tio"
|
|
|
|
|
|
def ligne_lisible(brut, amont_seul=False):
|
|
"""La ligne à écrire pour une ligne du journal, "" s'il n'y a rien.
|
|
|
|
Rend "" sur une ligne illisible et, sous « amont_seul », sur toute
|
|
requête qui n'est pas sortie vers l'internet.
|
|
"""
|
|
try:
|
|
d = json.loads(brut)
|
|
except (TypeError, ValueError):
|
|
return ""
|
|
if not isinstance(d, dict):
|
|
return ""
|
|
if amont_seul and not d.get("upstream"):
|
|
return ""
|
|
# L'horodatage du journal est en UTC, au format ISO : les positions 11 à
|
|
# 19 en sont l'heure.
|
|
heure = str(d.get("time", ""))[11:19] or "--:--:--"
|
|
client = str(d.get("client") or "—")
|
|
fleche = "↑" if d.get("upstream") else "·"
|
|
url = str(d.get("url", ""))
|
|
return (
|
|
f"{heure} {client:<15} {fleche} {d.get('status') or 0:>3} "
|
|
f"{str(d.get('outcome', '?')):<13}{taille(d.get('bytes')):>9} "
|
|
f"{url[-70:]}"
|
|
)
|
|
|
|
|
|
def main(argv=None, entree=None, sortie=None):
|
|
analyseur = argparse.ArgumentParser(
|
|
description="Met en forme le journal d'accès du cache QEMU."
|
|
)
|
|
analyseur.add_argument(
|
|
"--amont",
|
|
action="store_true",
|
|
help="ne montrer que les requêtes sorties vers l'internet",
|
|
)
|
|
args = analyseur.parse_args(argv)
|
|
entree = entree if entree is not None else sys.stdin
|
|
sortie = sortie if sortie is not None else sys.stdout
|
|
for brut in entree:
|
|
ligne = ligne_lisible(brut, args.amont)
|
|
if not ligne:
|
|
continue
|
|
sortie.write(ligne + "\n")
|
|
sortie.flush()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sys.exit(main())
|
|
except (BrokenPipeError, KeyboardInterrupt):
|
|
# Ctrl-C arrête le suivi, et « tail » fermé ferme le tube : ni l'un
|
|
# ni l'autre n'est une erreur à afficher.
|
|
sys.exit(0)
|