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
186 lines
5.2 KiB
Go
186 lines
5.2 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNormalizeMAC(t *testing.T) {
|
|
bons := map[string]string{
|
|
"52:54:00:AA:BB:CC": "52:54:00:aa:bb:cc",
|
|
"52-54-00-aa-bb-cc": "52:54:00:aa:bb:cc",
|
|
"525400aabbcc": "52:54:00:aa:bb:cc",
|
|
" 52:54:00:01:02:03 ": "52:54:00:01:02:03",
|
|
}
|
|
for brut, attendu := range bons {
|
|
got, err := NormalizeMAC(brut)
|
|
if err != nil {
|
|
t.Errorf("%q refusée : %v", brut, err)
|
|
continue
|
|
}
|
|
if got != attendu {
|
|
t.Errorf("%q → %q, attendu %q", brut, got, attendu)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Deux refus qui protègent d'une exception morte à l'écriture.
|
|
func TestNormalizeMACRefuse(t *testing.T) {
|
|
for _, brut := range []string{
|
|
"",
|
|
"pas une adresse",
|
|
"52:54:00:aa:bb", // cinq octets
|
|
"52:54:00:aa:bb:cc:dd:ee", // huit : une adresse InfiniBand
|
|
"00:00:00:00:00:00", // n'identifie rien
|
|
"ff:ff:ff:ff:ff:ff", // diffusion : jamais une source
|
|
"01:00:5e:00:00:01", // groupe : jamais une source
|
|
} {
|
|
if got, err := NormalizeMAC(brut); err == nil {
|
|
t.Errorf("%q acceptée et rendue %q", brut, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func fichierNeuf(t *testing.T) BypassFile {
|
|
t.Helper()
|
|
return BypassFile{Path: filepath.Join(t.TempDir(), "bypass")}
|
|
}
|
|
|
|
// Un fichier absent est l'état normal d'une installation neuve, pas une
|
|
// panne : le service doit démarrer.
|
|
func TestChargerUnFichierAbsent(t *testing.T) {
|
|
b := BypassFile{Path: filepath.Join(t.TempDir(), "jamais-ecrit")}
|
|
entries, err := b.Load()
|
|
if err != nil {
|
|
t.Fatalf("un fichier absent lève : %v", err)
|
|
}
|
|
if len(entries) != 0 {
|
|
t.Errorf("%d entrées sorties de rien", len(entries))
|
|
}
|
|
}
|
|
|
|
func TestAjouterPuisRetirer(t *testing.T) {
|
|
b := fichierNeuf(t)
|
|
mac, err := b.Add("52:54:00:AA:BB:CC", "vm-essai")
|
|
if err != nil {
|
|
t.Fatalf("ajout : %v", err)
|
|
}
|
|
if mac != "52:54:00:aa:bb:cc" {
|
|
t.Errorf("MAC rendue %q", mac)
|
|
}
|
|
entries, _ := b.Load()
|
|
if len(entries) != 1 || entries[0].Name != "vm-essai" {
|
|
t.Fatalf("relecture : %+v", entries)
|
|
}
|
|
// La forme d'écriture ne doit pas décider du retrait.
|
|
_, avait, err := b.Del("525400AABBCC")
|
|
if err != nil || !avait {
|
|
t.Fatalf("retrait : avait=%v err=%v", avait, err)
|
|
}
|
|
entries, _ = b.Load()
|
|
if len(entries) != 0 {
|
|
t.Errorf("%d entrée(s) survivent au retrait", len(entries))
|
|
}
|
|
}
|
|
|
|
// Le déploiement est relancé : la même VM ne doit pas produire deux lignes,
|
|
// sans quoi un seul retrait laisserait l'exception en place.
|
|
func TestReposerLaMemeMACMetAJour(t *testing.T) {
|
|
b := fichierNeuf(t)
|
|
if _, err := b.Add("52:54:00:aa:bb:cc", "ancien"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := b.Add("52:54:00:AA:BB:CC", "neuf"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := b.Load()
|
|
if len(entries) != 1 {
|
|
t.Fatalf("%d entrées pour une machine : %+v", len(entries), entries)
|
|
}
|
|
if entries[0].Name != "neuf" {
|
|
t.Errorf("nom resté %q", entries[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestRetirerCeQuiNyEstPas(t *testing.T) {
|
|
b := fichierNeuf(t)
|
|
mac, avait, err := b.Del("52:54:00:00:00:09")
|
|
if err != nil {
|
|
t.Fatalf("un retrait à vide lève : %v", err)
|
|
}
|
|
if avait {
|
|
t.Error("dit avoir retiré une exception absente")
|
|
}
|
|
if mac == "" {
|
|
t.Error("la MAC canonique doit être rendue pour le geste à chaud")
|
|
}
|
|
}
|
|
|
|
// Le fichier est éditable à la main : une faute de frappe ne doit pas
|
|
// empêcher le service de démarrer, seulement perdre sa ligne.
|
|
func TestUneLigneIllisibleEstSautee(t *testing.T) {
|
|
b := fichierNeuf(t)
|
|
contenu := strings.Join([]string{
|
|
"# un commentaire",
|
|
"",
|
|
"52:54:00:11:22:33 vm-une # en fin de ligne",
|
|
"ceci n'est pas une adresse",
|
|
"ff:ff:ff:ff:ff:ff diffusion",
|
|
"52:54:00:44:55:66",
|
|
"52:54:00:11:22:33 doublon",
|
|
}, "\n")
|
|
if err := os.WriteFile(b.Path, []byte(contenu), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, err := b.Load()
|
|
if err != nil {
|
|
t.Fatalf("lecture : %v", err)
|
|
}
|
|
if len(entries) != 2 {
|
|
t.Fatalf("%d entrées retenues : %+v", len(entries), entries)
|
|
}
|
|
if entries[0].MAC != "52:54:00:11:22:33" || entries[0].Name != "vm-une" {
|
|
t.Errorf("première entrée : %+v", entries[0])
|
|
}
|
|
if entries[1].MAC != "52:54:00:44:55:66" {
|
|
t.Errorf("seconde entrée : %+v", entries[1])
|
|
}
|
|
}
|
|
|
|
// Le nom voyage avec la MAC : c'est lui qui dit quelle entrée retirer quand
|
|
// la VM meurt. Une entrée orpheline soustrairait au cache une machine neuve
|
|
// qui hériterait de la MAC, sans que personne l'ait demandé.
|
|
func TestLeNomSurvitAuxAllersRetours(t *testing.T) {
|
|
b := fichierNeuf(t)
|
|
if _, err := b.Add("52:54:00:77:88:99", "vm de démonstration"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := b.Load()
|
|
if len(entries) != 1 || entries[0].Name != "vm de démonstration" {
|
|
t.Fatalf("nom perdu : %+v", entries)
|
|
}
|
|
}
|
|
|
|
func TestGestesAChaud(t *testing.T) {
|
|
mac := "52:54:00:aa:bb:cc"
|
|
ajout := BypassAddElement(mac)
|
|
retrait := BypassDelElement(mac)
|
|
for _, l := range []string{ajout, retrait} {
|
|
if !strings.Contains(l, TableName) ||
|
|
!strings.Contains(l, BypassSetName) ||
|
|
!strings.Contains(l, mac) {
|
|
t.Errorf("geste incomplet : %q", l)
|
|
}
|
|
}
|
|
if !strings.HasPrefix(ajout, "add element") {
|
|
t.Errorf("ajout : %q", ajout)
|
|
}
|
|
if !strings.HasPrefix(retrait, "delete element") {
|
|
t.Errorf("retrait : %q", retrait)
|
|
}
|
|
}
|