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
141 lines
4.8 KiB
Go
141 lines
4.8 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"
|
|
"testing"
|
|
)
|
|
|
|
// vendredi et samedi : deux instantanés d'un dépôt apt, à un jour d'écart.
|
|
const (
|
|
vendredi = "Fri, 11 Sep 2026 07:18:59 GMT"
|
|
samedi = "Sat, 12 Sep 2026 13:06:19 GMT"
|
|
)
|
|
|
|
// garnir range un corps sous son URL, avec la date que l'amont lui donne.
|
|
func garnir(t *testing.T, p *Proxy, brut, corps, lastModified string) {
|
|
t.Helper()
|
|
u, err := url.Parse(brut)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w, err := p.Store.NewWriter(CleDe("GET", u), Meta{
|
|
URL: u.String(), Method: "GET", Status: http.StatusOK,
|
|
Header: http.Header{"Last-Modified": []string{lastModified}},
|
|
Class: ClassVolatile.String(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := w.Write([]byte(corps)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := w.Commit(int64(len(corps))); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Un index plus récent que la signature qui l'annonce n'est pas servi.
|
|
//
|
|
// apt exige de chaque index la taille et la somme écrites dans la
|
|
// signature : un index pris à un autre instantané le fait échouer sur
|
|
// « File has unexpected size », et l'installation s'arrête plus bas sur des
|
|
// dépendances introuvables — un message qui accuse le dépôt, jamais le
|
|
// cache.
|
|
func TestUnIndexPlusRecentQueSaSignatureNeSortPas(t *testing.T) {
|
|
a := nouvelAmont(t, "peu importe")
|
|
p := proxyDeTest(t)
|
|
base := "http://" + a.hote() + "/ubuntu/dists/resolute-updates/"
|
|
garnir(t, p, base+"InRelease", "signature de vendredi", vendredi)
|
|
garnir(t, p, base+"main/binary-amd64/Packages.xz", "index de samedi", samedi)
|
|
hote := a.hote()
|
|
a.srv.Close()
|
|
|
|
w := joue(t, p, "GET", hote, "/ubuntu/dists/resolute-updates/InRelease")
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("la signature gardée doit sortir : code %d", w.Code)
|
|
}
|
|
w = joue(t, p, "GET", hote,
|
|
"/ubuntu/dists/resolute-updates/main/binary-amd64/Packages.xz")
|
|
if w.Code == http.StatusOK {
|
|
t.Errorf("l'index de samedi est sorti sous la signature de vendredi")
|
|
}
|
|
}
|
|
|
|
// L'inverse est le cas NORMAL : les deux viennent du même passage en ligne,
|
|
// et refuser de servir là romprait le déploiement hors ligne.
|
|
func TestUnIndexAussiVieuxQueSaSignatureSort(t *testing.T) {
|
|
a := nouvelAmont(t, "peu importe")
|
|
p := proxyDeTest(t)
|
|
base := "http://" + a.hote() + "/ubuntu/dists/resolute/"
|
|
garnir(t, p, base+"InRelease", "signature", vendredi)
|
|
garnir(t, p, base+"main/binary-amd64/Packages.xz", "index", vendredi)
|
|
hote := a.hote()
|
|
a.srv.Close()
|
|
|
|
w := joue(t, p, "GET", hote,
|
|
"/ubuntu/dists/resolute/main/binary-amd64/Packages.xz")
|
|
if w.Code != http.StatusOK || w.Body.String() != "index" {
|
|
t.Fatalf("code %d, corps %q : l'index du même instantané doit sortir",
|
|
w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Un objet adressé par son contenu ne se juge pas : son nom EST sa somme,
|
|
// et apt l'accepte quelle que soit la date de la signature.
|
|
func TestUnObjetParEmpreinteSortQuandMeme(t *testing.T) {
|
|
a := nouvelAmont(t, "peu importe")
|
|
p := proxyDeTest(t)
|
|
base := "http://" + a.hote() + "/ubuntu/dists/resolute-updates/"
|
|
garnir(t, p, base+"InRelease", "signature de vendredi", vendredi)
|
|
parHash := base + "main/binary-amd64/by-hash/SHA256/" +
|
|
"e55707cc7a5ad03d7f700ccb6646fd5c6ecbc852c4c826f5edbb5a321358d0ee"
|
|
garnir(t, p, parHash, "index de samedi", samedi)
|
|
hote := a.hote()
|
|
a.srv.Close()
|
|
|
|
w := joue(t, p, "GET", hote,
|
|
"/ubuntu/dists/resolute-updates/main/binary-amd64/by-hash/SHA256/"+
|
|
"e55707cc7a5ad03d7f700ccb6646fd5c6ecbc852c4c826f5edbb5a321358d0ee")
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("un objet par empreinte a été retenu : code %d", w.Code)
|
|
}
|
|
}
|
|
|
|
// Sans signature gardée, il n'y a rien à contredire : l'index gardé est tout
|
|
// ce que le hors-ligne possède, et le retenir ne servirait personne.
|
|
func TestSansSignatureLIndexSortQuandMeme(t *testing.T) {
|
|
a := nouvelAmont(t, "peu importe")
|
|
p := proxyDeTest(t)
|
|
garnir(t, p, "http://"+a.hote()+
|
|
"/ubuntu/dists/orpheline/main/binary-amd64/Packages.xz",
|
|
"index seul", samedi)
|
|
hote := a.hote()
|
|
a.srv.Close()
|
|
|
|
w := joue(t, p, "GET", hote,
|
|
"/ubuntu/dists/orpheline/main/binary-amd64/Packages.xz")
|
|
if w.Code != http.StatusOK || w.Body.String() != "index seul" {
|
|
t.Errorf("code %d, corps %q : l'index sans signature doit sortir",
|
|
w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
// Hors de « /dists/ », aucune signature n'entre en jeu : un paquet ne se
|
|
// juge pas sur la date d'un index.
|
|
func TestHorsDeDistsRienNestJuge(t *testing.T) {
|
|
a := nouvelAmont(t, "peu importe")
|
|
p := proxyDeTest(t)
|
|
garnir(t, p, "http://"+a.hote()+"/ubuntu/pool/main/x/xz_5.6_amd64.deb",
|
|
"le paquet", samedi)
|
|
hote := a.hote()
|
|
a.srv.Close()
|
|
|
|
w := joue(t, p, "GET", hote, "/ubuntu/pool/main/x/xz_5.6_amd64.deb")
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("un paquet a été retenu : code %d", w.Code)
|
|
}
|
|
}
|