Rien n'invalidait une entrée dont la somme ne correspond pas : le magasin continuait de la servir, et retélécharger ne changeait rien puisque c'est lui qui répond. Les deux purges existantes ne l'atteignent pas — l'une efface tout, l'autre saute ce qui est récent alors que chaque service remet cette date, si bien qu'un objet empoisonné qui sert ne vieillit jamais. Symétrique de « --detient » : mêmes lignes, mêmes fonctions de clé, mêmes refus, et un objet présent qui résiste est dit refusé plutôt qu'oublié. Mesuré : garde, puis oublié 53080, puis absent. --- EN --- Nothing invalidated an entry whose checksum does not match: the store kept serving it, and re-downloading changed nothing since the store is what answers. Neither existing purge reaches it — one erases everything, the other skips what is recent while every service resets that date, so a poisoned object that serves never ages. Symmetric to "--detient": same lines, same key functions, same refusals, and a present object that resists is reported refused rather than forgotten. Measured: held, then forgotten 53080, then absent. Assisted-by: Claude Opus 5
516 lines
18 KiB
Go
516 lines
18 KiB
Go
// © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
// License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
// erplibre_go_qemu_cache — miroir de téléchargement pour les VM QEMU locales.
|
|
//
|
|
// L'orchestrateur détourne le 80 et le 443 de ses VM vers cet outil, qui
|
|
// garde ce qui descend et sert la copie à la VM suivante. Deux VM de la même
|
|
// distribution ne tirent donc qu'une fois les mêmes paquets.
|
|
//
|
|
// Ce que l'outil NE fait pas, et qu'il faut savoir : il n'efface rien. Aucune
|
|
// éviction, aucun plafond de disque — « --status » dit ce qu'il occupe, la
|
|
// surveillance est manuelle et le cache vit sur le disque de l'orchestrateur.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const version = "0.2.12"
|
|
|
|
func main() {
|
|
var (
|
|
cacheDir = flag.String("cache-dir", "/var/cache/erplibre_go_qemu_cache",
|
|
T("répertoire des objets gardés"))
|
|
caDir = flag.String("ca-dir", "/var/lib/erplibre_go_qemu_cache",
|
|
T("répertoire de l'autorité de certification"))
|
|
httpPort = flag.Int("http-port", 8898, T("écoute pour le 80 détourné"))
|
|
tlsPort = flag.Int("tls-port", 8899, T("écoute pour le 443 détourné"))
|
|
bridge = flag.String("bridge", "virbr0", T("pont libvirt des VM"))
|
|
subnet = flag.String("subnet", "192.168.122.0/24", T("sous-réseau des VM"))
|
|
logPath = flag.String("access-log", "", T("journal d'accès JSON par ligne"))
|
|
exclude = flag.String("exclude", "",
|
|
T("hôtes à ne jamais déchiffrer, séparés par des virgules"))
|
|
verbose = flag.Bool("verbose", false, T("dire chaque requête"))
|
|
status = flag.Bool("status", false,
|
|
T("dire ce que le cache occupe, puis sortir"))
|
|
dryRun = flag.Bool("dry-run", false,
|
|
T("montrer les gestes privilégiés sans en faire un"))
|
|
initCA = flag.Bool("init-ca", false,
|
|
T("créer l'autorité si elle manque, puis sortir"))
|
|
printNft = flag.Bool("print-nft", false,
|
|
T("écrire les règles nft seules, à passer à « nft -f - »"))
|
|
printIptables = flag.Bool("print-iptables", false,
|
|
T("écrire les commandes iptables seules, une par ligne"))
|
|
bypassFile = flag.String("bypass-file",
|
|
"/etc/erplibre_go_qemu_cache/bypass",
|
|
T("liste des VM soustraites au détournement, une MAC par ligne"))
|
|
bypassAdd = flag.String("bypass-add", "",
|
|
T("soustraire cette adresse MAC au détournement"))
|
|
bypassName = flag.String("bypass-name", "",
|
|
T("nom de la VM, écrit à côté de la MAC ajoutée"))
|
|
bypassDel = flag.String("bypass-del", "",
|
|
T("rendre cette adresse MAC au détournement"))
|
|
gitMirrorDir = flag.String("git-mirror-dir", "",
|
|
T("racine des dépôts git tenus en miroir ; vide, git est"+
|
|
" simplement relayé vers l'amont"))
|
|
gitMirrorFresh = flag.Duration("git-mirror-fresh", 60*time.Second,
|
|
T("délai en deçà duquel un dépôt n'est pas re-interrogé"))
|
|
gitPrefetch = flag.String("git-mirror-prefetch", "",
|
|
T("fichier de dépôts, un par ligne, à tenir en miroir d'avance"))
|
|
gitPrefetchJobs = flag.Int("git-mirror-jobs", 4,
|
|
T("dépôts clonés en parallèle par le pré-remplissage"))
|
|
ageReport = flag.Bool("age-report", false,
|
|
T("dire ce que le cache occupe, groupé par âge du dernier usage"))
|
|
agePar = flag.String("age-par", "semaine",
|
|
T("découpage du relevé par âge : jour, semaine ou mois"))
|
|
purgeTout = flag.Bool("purge", false,
|
|
T("effacer TOUT le cache : objets et dépôts en miroir"))
|
|
purgeAvant = flag.String("purge-older-than", "",
|
|
T("n'effacer que ce qui n'a pas servi depuis ce délai (ex. 30j, 12h)"))
|
|
purgeTaille = flag.String("purge-to-size", "",
|
|
T("effacer le moins récemment servi jusqu'à tenir sous cette taille (ex. 50G)"))
|
|
gitList = flag.Bool("git-mirror-list", false,
|
|
T("dire les dépôts tenus en miroir, du plus lourd au plus léger"))
|
|
gitRemove = flag.String("git-mirror-remove", "",
|
|
T("effacer le miroir de ce dépôt ; il se refera au prochain besoin"))
|
|
bypassList = flag.Bool("bypass-list", false,
|
|
T("dire les exceptions en place, une « MAC nom » par ligne"))
|
|
showVersion = flag.Bool("version", false, T("dire la version, puis sortir"))
|
|
lang = flag.String("lang", "", T("langue des messages : fr ou en ; à défaut, EL_LANG"))
|
|
detient = flag.Bool("detient", false,
|
|
T("lire des lignes « MÉTHODE URL » sur l'entrée standard et dire,"+
|
|
" pour chacune, ce que le magasin tient : une ligne séparée"+
|
|
" par des tabulations « verdict statut stored_at classe"+
|
|
" méthode url », verdict garde (corps 200), statut (statut"+
|
|
" seul, sans corps), absent ou non-cachable. Lecture seule :"+
|
|
" --cache-dir suffit, sans privilège, et l'âge des objets"+
|
|
" n'est pas touché"))
|
|
oublie = flag.Bool("oublie", false,
|
|
T("lire des lignes « MÉTHODE URL » sur l'entrée standard et"+
|
|
" EFFACER du magasin ce que chacune désigne : le corps ET le"+
|
|
" statut seul. Dit, par ligne et séparé par des tabulations,"+
|
|
" « verdict octets classe méthode url », verdict oublié,"+
|
|
" absent, non-cachable ou refus. Le seul geste qui retire UNE"+
|
|
" entrée : --purge efface tout, et --purge-older-than"+
|
|
" n'atteint jamais un objet qui sert, chaque service remettant"+
|
|
" sa date à maintenant. Écrit : demande le droit du service."+
|
|
" Pas d'essai à blanc — --detient EST l'aperçu : il répond aux"+
|
|
" mêmes lignes, par les mêmes clés, sans rien modifier"))
|
|
)
|
|
flag.Parse()
|
|
// Déjà lue dans os.Args avant l'analyse (voir Langue) ; posée ici aussi
|
|
// pour que la valeur retenue soit celle que flag a comprise.
|
|
if *lang != "" {
|
|
definirLangue(*lang)
|
|
}
|
|
|
|
if *showVersion {
|
|
fmt.Printf("erplibre_go_qemu_cache %s\n", version)
|
|
return
|
|
}
|
|
// Traité AVANT tout ce qui lit une configuration : la question ne
|
|
// porte que sur le magasin, et un fichier d'exceptions illisible pour
|
|
// l'appelant ne doit pas l'empêcher d'y répondre.
|
|
if *detient {
|
|
store := &Store{Dir: *cacheDir}
|
|
if err := EcrireDetentions(store, os.Stdin, os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, T("entrée illisible : %v\n"), err)
|
|
}
|
|
return
|
|
}
|
|
// Au même endroit et pour la même raison : la question ne porte que sur
|
|
// le magasin. Écrire plutôt que lire ne change pas de quoi elle dépend.
|
|
if *oublie {
|
|
store := &Store{Dir: *cacheDir}
|
|
if err := EcrireOublis(store, os.Stdin, os.Stdout); err != nil {
|
|
fmt.Fprintf(os.Stderr, "entrée illisible : %v\n", err)
|
|
}
|
|
return
|
|
}
|
|
|
|
bypass := BypassFile{Path: *bypassFile}
|
|
// Les exceptions entrent dans les règles dès leur RENDU : le service les
|
|
// repose telles quelles à chaque démarrage, et une exception ne survit
|
|
// donc pas au seul noyau.
|
|
exceptions, err := bypass.Load()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("exceptions illisibles : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
rules := RuleSet{
|
|
Bridge: *bridge, Subnet: *subnet,
|
|
HTTPPort: *httpPort, TLSPort: *tlsPort,
|
|
Bypass: MACs(exceptions),
|
|
}
|
|
store := &Store{Dir: *cacheDir}
|
|
|
|
// Les règles sortent d'ici et de nulle part ailleurs : le service les
|
|
// pose en tubant cette sortie dans nft. Une seconde copie dans un script
|
|
// d'installation dériverait de celle que les tests vérifient.
|
|
if *printNft {
|
|
for _, l := range rules.NftLines() {
|
|
fmt.Println(l)
|
|
}
|
|
return
|
|
}
|
|
if *printIptables {
|
|
for _, l := range rules.IptablesLines() {
|
|
fmt.Println(l)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Les trois gestes de la liste écrivent le fichier et rendent sur la
|
|
// SORTIE le geste à chaud correspondant, à tuber dans « nft -f - ». Ce
|
|
// paquet ne touche pas au pare-feu : c'est l'invariant qui rend les
|
|
// règles vérifiables par un test sans privilège.
|
|
if *bypassAdd != "" {
|
|
mac, err := bypass.Add(*bypassAdd, *bypassName)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("exception refusée : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Fprintf(os.Stderr, T("exception posée : %s %s\n"), mac, *bypassName)
|
|
fmt.Println(BypassAddElement(mac))
|
|
return
|
|
}
|
|
if *bypassDel != "" {
|
|
mac, avait, err := bypass.Del(*bypassDel)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("exception refusée : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
if !avait {
|
|
fmt.Fprintf(os.Stderr, T("aucune exception pour %s\n"), mac)
|
|
}
|
|
// Le geste à chaud est rendu même si le fichier ne l'avait pas :
|
|
// l'ensemble du noyau peut porter ce que le fichier a perdu, et le
|
|
// retrait doit alors pouvoir le rattraper.
|
|
fmt.Println(BypassDelElement(mac))
|
|
return
|
|
}
|
|
if *bypassList {
|
|
for _, e := range exceptions {
|
|
fmt.Printf("%s %s\n", e.MAC, e.Name)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Une seule mémoire des amonts muets, partagée par le relais et le
|
|
// miroir : un hôte coupé l'est pour les deux, et le premier qui le
|
|
// constate en épargne le délai à l'autre.
|
|
muets := NouvelleJoignabilite()
|
|
// Le témoin que la levée d'une coupure touche. Il vit dans le magasin,
|
|
// seul répertoire que le service et la levée connaissent tous deux.
|
|
muets.Sentinelle = filepath.Join(*cacheDir, SentinelleAmonts)
|
|
miroir := &GitMirror{
|
|
Dir: *gitMirrorDir, Frais: *gitMirrorFresh, Muets: muets,
|
|
}
|
|
|
|
if *ageReport {
|
|
gran, err := LireGranularite(*agePar)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
printAge(store, miroir, gran)
|
|
return
|
|
}
|
|
if *purgeTout || *purgeAvant != "" {
|
|
avant := time.Now()
|
|
if *purgeAvant != "" {
|
|
d, err := LireDuree(*purgeAvant)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
avant = time.Now().Add(-d)
|
|
fmt.Printf(T("efface ce qui n'a pas servi depuis %s (avant %s)\n"),
|
|
*purgeAvant, avant.Format("2006-01-02 15:04"))
|
|
} else {
|
|
fmt.Println(T("efface TOUT le cache"))
|
|
}
|
|
if *dryRun {
|
|
printPurgeABlanc(store, miroir, avant)
|
|
return
|
|
}
|
|
n, oct, err := store.Purger(avant)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("purge des objets : %v\n"), err)
|
|
}
|
|
fmt.Printf(T("objets effacés : %d, %s rendus\n"), n, HumanBytes(oct))
|
|
nd, octd, err := miroir.PurgerMiroirs(avant)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("purge des miroirs : %v\n"), err)
|
|
}
|
|
fmt.Printf(T("dépôts effacés : %d, %s rendus\n"), nd, HumanBytes(octd))
|
|
return
|
|
}
|
|
if *purgeTaille != "" {
|
|
plafond, err := LireTaille(*purgeTaille)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
b, err := PurgerJusqua(store, miroir, plafond, *dryRun)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("purge au plafond : %v\n"), err)
|
|
}
|
|
prefixe := ""
|
|
if *dryRun {
|
|
prefixe = T("[à blanc] ")
|
|
}
|
|
fmt.Printf(T("plafond %s : le cache occupe %s\n"), HumanBytes(plafond), HumanBytes(b.Avant))
|
|
fmt.Printf(T("%sobjets : %d, %s ; dépôts : %d, %s ; reste %s\n"), prefixe,
|
|
b.Objets, HumanBytes(b.OctetsObjets), b.Depots, HumanBytes(b.OctetsDepots),
|
|
HumanBytes(b.Apres))
|
|
return
|
|
}
|
|
if *gitList {
|
|
depots := miroir.Depots()
|
|
if len(depots) == 0 {
|
|
fmt.Println(T("aucun dépôt en miroir"))
|
|
return
|
|
}
|
|
var total int64
|
|
for _, d := range depots {
|
|
total += d.Octets
|
|
fmt.Printf("%10s %s %s\n",
|
|
HumanBytes(d.Octets), d.Maj.Format("2006-01-02"), d.Nom)
|
|
}
|
|
fmt.Printf(T("%10s %d dépôts\n"), HumanBytes(total), len(depots))
|
|
return
|
|
}
|
|
if *gitRemove != "" {
|
|
// Le nom suffit : on ne demande pas à l'opérateur de retrouver le
|
|
// chemin d'un répertoire qu'il n'a pas choisi.
|
|
cible := *gitRemove
|
|
if !strings.HasPrefix(cible, miroir.Dir) {
|
|
for _, d := range miroir.Depots() {
|
|
if d.Nom == strings.TrimSuffix(cible, ".git") {
|
|
cible = d.Chemin
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if err := miroir.Retirer(cible); err != nil {
|
|
fmt.Fprintf(os.Stderr, T("effacement : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf(T("miroir effacé : %s\n"), cible)
|
|
return
|
|
}
|
|
if *gitPrefetch != "" {
|
|
if !miroir.Actif() {
|
|
fmt.Fprintln(os.Stderr,
|
|
T("miroir git éteint : passer --git-mirror-dir"))
|
|
os.Exit(1)
|
|
}
|
|
depots, err := DepotsDuFichier(*gitPrefetch)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("liste illisible : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf(T("%d dépôts à tenir en miroir sous %s\n"),
|
|
len(depots), miroir.Dir)
|
|
reussis, echoues := miroir.Prefetch(
|
|
context.Background(), depots, *gitPrefetchJobs,
|
|
func(l string) { fmt.Println(l) },
|
|
)
|
|
_, octets := miroir.Occupation()
|
|
fmt.Printf(T("%d en miroir, %d en échec, %s occupés\n"),
|
|
reussis, echoues, HumanBytes(octets))
|
|
// Un dépôt mort ne fait pas échouer l'opération : sur une liste de
|
|
// trois cents, il y en a toujours un — privé, déplacé, retiré — et
|
|
// rendre une erreur ferait passer pour ratée une avance qui a pris.
|
|
// Seule une liste dont RIEN n'a été tenu est un échec.
|
|
if reussis == 0 && echoues > 0 {
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if *status {
|
|
if err := printStatus(store, *caDir, rules, miroir); err != nil {
|
|
fmt.Fprintf(os.Stderr, T("état illisible : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
return
|
|
}
|
|
|
|
if *dryRun {
|
|
printDryRun(store, *caDir, rules)
|
|
return
|
|
}
|
|
|
|
if *initCA {
|
|
ca, err := LoadOrCreateCA(*caDir)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, T("autorité : %v\n"), err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Printf(T("autorité : %s\n"), CertPath(*caDir))
|
|
fmt.Printf(T("empreinte : %s\n"), ca.Fingerprint())
|
|
return
|
|
}
|
|
|
|
if *gitMirrorDir != "" && !miroir.Actif() {
|
|
// Le dire plutôt que de laisser croire à un miroir : sans le
|
|
// programme de git, le service marche mais git repart à l'amont à
|
|
// chaque VM, ce qui est exactement ce que le miroir devait éviter.
|
|
log.Printf(
|
|
T("miroir git demandé mais « git-http-backend » est introuvable :"+
|
|
" git sera relayé vers l'amont (%s)"), *gitMirrorDir)
|
|
}
|
|
if err := serve(
|
|
store, *caDir, rules, *logPath, *exclude, *verbose, miroir, muets,
|
|
); err != nil {
|
|
log.Fatalf(T("le cache s'arrête : %v"), err)
|
|
}
|
|
}
|
|
|
|
func serve(
|
|
store *Store, caDir string, rules RuleSet,
|
|
logPath, exclude string, verbose bool, miroir *GitMirror,
|
|
muets *Joignabilite,
|
|
) error {
|
|
if err := os.MkdirAll(store.Dir, 0o755); err != nil {
|
|
return err
|
|
}
|
|
// Un objet à moitié écrit ne vaut rien et occupe : le démarrage est le
|
|
// seul moment où un « .part » n'appartient à aucune écriture vivante.
|
|
if n := store.SweepPartials(); n > 0 {
|
|
log.Printf(T("%d écriture(s) interrompue(s) retirée(s)"), n)
|
|
}
|
|
|
|
ca, err := LoadOrCreateCA(caDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
alog, err := OpenAccessLog(logPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer alog.Close()
|
|
|
|
proxy := NewProxy(store, alog)
|
|
proxy.Git = miroir
|
|
proxy.Verbose = verbose
|
|
proxy.Muets = muets
|
|
// Les ports d'écoute sont ceux que la requête d'une boucle viserait.
|
|
proxy.Ecoutes = []int{rules.HTTPPort, rules.TLSPort}
|
|
|
|
refusals := NewRefusals(append(DefaultExclusions, splitList(exclude)...))
|
|
front := &TLSFront{CA: ca, Proxy: proxy, Refusals: refusals}
|
|
|
|
httpLn, err := net.Listen("tcp", fmt.Sprintf(":%d", rules.HTTPPort))
|
|
if err != nil {
|
|
return fmt.Errorf(T("écoute HTTP : %w"), err)
|
|
}
|
|
tlsLn, err := net.Listen("tcp", fmt.Sprintf(":%d", rules.TLSPort))
|
|
if err != nil {
|
|
return fmt.Errorf(T("écoute TLS : %w"), err)
|
|
}
|
|
|
|
log.Printf(T("cache : %s"), store.Dir)
|
|
log.Printf(T("autorité : %s (%s)"), CertPath(caDir), ca.Fingerprint())
|
|
log.Printf(T("écoutes : http %d, tls %d"), rules.HTTPPort, rules.TLSPort)
|
|
|
|
errc := make(chan error, 2)
|
|
go func() {
|
|
srv := &http.Server{
|
|
Handler: proxy.handler("http"),
|
|
ReadHeaderTimeout: 30 * time.Second,
|
|
}
|
|
errc <- srv.Serve(httpLn)
|
|
}()
|
|
go func() { errc <- front.Serve(tlsLn) }()
|
|
return <-errc
|
|
}
|
|
|
|
func printStatus(
|
|
store *Store, caDir string, rules RuleSet, miroirStatut *GitMirror,
|
|
) error {
|
|
st, err := store.Stat()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Printf(T("répertoire : %s\n"), store.Dir)
|
|
fmt.Printf(T("objets : %d\n"), st.Objects)
|
|
fmt.Printf(T("occupation : %s\n"), HumanBytes(st.Bytes))
|
|
if st.Oldest.IsZero() {
|
|
fmt.Printf("%s", T("plus ancien : aucun objet\n"))
|
|
} else {
|
|
fmt.Printf(T("plus ancien : %s\n"), st.Oldest.Format(time.RFC3339))
|
|
}
|
|
fmt.Printf(T("autorité : %s\n"), CertPath(caDir))
|
|
if ca, err := LoadOrCreateCA(caDir); err == nil {
|
|
fmt.Printf(T("empreinte : %s\n"), ca.Fingerprint())
|
|
} else {
|
|
fmt.Printf("%s", T("empreinte : autorité absente\n"))
|
|
}
|
|
if depots, octets := miroirStatut.Occupation(); depots > 0 {
|
|
fmt.Printf(T("dépôts git : %d en miroir, %s\n"),
|
|
depots, HumanBytes(octets))
|
|
}
|
|
if len(rules.Bypass) == 0 {
|
|
fmt.Printf("%s", T("exceptions : aucune\n"))
|
|
} else {
|
|
fmt.Printf(T("exceptions : %d VM soustraite(s) au détournement\n"),
|
|
len(rules.Bypass))
|
|
for _, m := range rules.Bypass {
|
|
fmt.Printf(" %s\n", m)
|
|
}
|
|
}
|
|
fmt.Printf("%s", T("\nAucune éviction n'est écrite : ce cache ne diminue jamais\n"))
|
|
fmt.Printf("%s", T("de lui-même, et il vit sur le disque de l'orchestrateur.\n"))
|
|
return nil
|
|
}
|
|
|
|
func printDryRun(store *Store, caDir string, rules RuleSet) {
|
|
fmt.Printf("%s", T("À blanc — rien n'est écrit, rien n'est posé.\n\n"))
|
|
fmt.Printf(T("Répertoire du cache, créé au démarrage :\n %s\n\n"), store.Dir)
|
|
fmt.Printf(T("Autorité, créée si elle manque :\n %s (clé en 0600)\n\n"),
|
|
CertPath(caDir))
|
|
fmt.Printf("%s", T("Règles nft à poser sur l'hôte :\n"))
|
|
for _, l := range rules.NftLines() {
|
|
fmt.Printf(" %s\n", l)
|
|
}
|
|
fmt.Printf("%s", T("\nÀ défaut de nft :\n"))
|
|
for _, l := range rules.IptablesLines() {
|
|
fmt.Printf(" %s\n", l)
|
|
}
|
|
fmt.Printf(T("\nRetrait :\n %s\n"), rules.NftDeleteLine())
|
|
fmt.Printf("%s", T("\nDans chaque VM qui utilise le cache :\n"))
|
|
for _, f := range []string{"pacman", "apt", "dnf", "zypper"} {
|
|
dir, cmd, bundle, _ := GuestTrustCommand(f)
|
|
fmt.Printf(T(" %-7s %s/erplibre-cache.crt puis %s\n"), f, dir, cmd)
|
|
for _, l := range GuestEnvLines(bundle) {
|
|
fmt.Printf(" %s\n", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func splitList(s string) []string {
|
|
if strings.TrimSpace(s) == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(s, ",")
|
|
out := make([]string, 0, len(parts))
|
|
for _, p := range parts {
|
|
if p = strings.TrimSpace(p); p != "" {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out
|
|
}
|