erplibre/script/qemu_cache/instantane.go
Mathieu Benoit a7706adf3d [ADD] cache qemu : miroir des téléchargements des VM, hors ligne compris
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
2026-09-14 16:46:15 -04:00

91 lines
3 KiB
Go

// © 2026 TechnoLibre (http://www.technolibre.ca)
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
package main
import (
"net/http"
"net/url"
"regexp"
"strings"
"time"
)
// dossierSuite isole « …/dists/<suite>/ » du chemin d'un index apt.
var dossierSuite = regexp.MustCompile(`^(.*/dists/[^/]+/)`)
// indexIncoherent dit si le corps gardé sous cette clé appartient à un
// instantané PLUS RÉCENT que la signature gardée pour la même suite.
//
// apt lit d'abord « InRelease », puis exige de chaque index la taille et la
// somme qui y sont écrites. Hors ligne, chaque objet sort du magasin avec sa
// propre date : un index rafraîchi samedi, servi sous une signature gardée
// depuis vendredi, fait échouer apt sur « File has unexpected size » ou
// « Hash Sum mismatch ». L'installation s'arrête plus bas sur des
// dépendances introuvables — un message qui accuse le dépôt, jamais le
// cache. Ne rien servir vaut mieux : apt garde alors ses propres listes,
// dit « old ones used instead » et poursuit.
//
// La comparaison porte sur le « Last-Modified » de l'AMONT, jamais sur la
// date de stockage : celle-ci est renouvelée à chaque service, si bien
// qu'une signature servie après l'index paraîtrait plus jeune que lui.
//
// Trois objets échappent au jugement : ceux qui sont adressés par leur
// contenu (« by-hash »), dont le nom EST la somme ; les signatures
// elles-mêmes, qui sont l'étalon ; et tout ce qui ne vit pas sous
// « /dists/ ».
func (p *Proxy) indexIncoherent(u *url.URL, key string) bool {
if p == nil || p.Store == nil || u == nil {
return false
}
if strings.Contains(u.Path, "/by-hash/") {
return false
}
if strings.HasSuffix(u.Path, "/InRelease") ||
strings.HasSuffix(u.Path, "/Release") ||
strings.HasSuffix(u.Path, "/Release.gpg") {
return false
}
trouve := dossierSuite.FindStringSubmatch(u.Path)
if trouve == nil {
return false
}
signature := *u
signature.Path = trouve[1] + "InRelease"
signature.RawQuery = ""
metaSig, err := p.Store.LireMeta(CleDe("GET", &signature))
if err != nil {
// Aucune signature gardée : rien à contredire, et l'index gardé est
// tout ce que le hors-ligne possède.
return false
}
metaIdx, err := p.Store.LireMeta(key)
if err != nil {
return false
}
dateSig, okSig := dateAmont(metaSig)
dateIdx, okIdx := dateAmont(metaIdx)
if !okSig || !okIdx {
// Sans les deux dates, on ne sait pas : servir reste le comportement
// qui rend un déploiement hors ligne possible.
return false
}
return dateIdx.After(dateSig)
}
// dateAmont rend le « Last-Modified » écrit par l'amont, et dit s'il y en a
// un. C'est la seule date que le magasin ne retouche jamais.
func dateAmont(m *Meta) (t time.Time, ok bool) {
if m == nil || m.Header == nil {
return time.Time{}, false
}
brut := http.Header(m.Header).Get("Last-Modified")
if brut == "" {
return time.Time{}, false
}
quand, err := http.ParseTime(brut)
if err != nil {
return time.Time{}, false
}
return quand, true
}