erplibre/script/install/install_qemu_cache.sh
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

358 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
#
# Pose erplibre_go_qemu_cache : le miroir de téléchargement des VM QEMU
# locales. Compile depuis les sources du dépôt, installe une unité systemd,
# et laisse le service poser lui-même ses règles de détournement.
#
# Les règles de pare-feu ne sont PAS écrites par ce script. L'unité les pose
# à son démarrage et les retire à son arrêt, en tubant « --print-nft » dans
# nft : une VM ne peut donc pas rester détournée vers un cache éteint, et le
# jeu de règles vient du code Go que les tests vérifient, jamais d'une copie.
#
# Usage :
# sudo bash install_qemu_cache.sh
# sudo EL_HTTP_PORT=8898 EL_TLS_PORT=8899 EL_BRIDGE=virbr0 \
# EL_SUBNET=192.168.122.0/24 bash install_qemu_cache.sh
set -e
EL_HTTP_PORT="${EL_HTTP_PORT:-8898}"
EL_TLS_PORT="${EL_TLS_PORT:-8899}"
# Vides par défaut : le pont et le sous-réseau sont LUS dans libvirt plus bas.
# Les supposer est le défaut qui rend l'installation silencieusement inutile —
# un hôte dont le réseau par défaut a été déplacé sur un /24 libre, ce qui
# arrive dès que 192.168.122 entre en collision, voit ses VM échapper au
# détournement sans que rien ne le signale.
EL_BRIDGE="${EL_BRIDGE:-}"
EL_SUBNET="${EL_SUBNET:-}"
EL_NET="${EL_NET:-default}"
EL_CACHE_DIR="${EL_CACHE_DIR:-/var/cache/erplibre_go_qemu_cache}"
EL_CA_DIR="${EL_CA_DIR:-/var/lib/erplibre_go_qemu_cache}"
EL_ACCESS_LOG="${EL_ACCESS_LOG:-/var/log/erplibre_go_qemu_cache.jsonl}"
EL_EXCLUDE="${EL_EXCLUDE:-}"
EL_BYPASS_FILE="${EL_BYPASS_FILE:-/etc/erplibre_go_qemu_cache/bypass}"
# Les dépôts git tenus en miroir. Sous le répertoire du cache : c'est le
# seul que le service peut écrire, et le miroir grossit comme le reste.
EL_GIT_MIRROR_DIR="${EL_GIT_MIRROR_DIR:-${EL_CACHE_DIR:-/var/cache/erplibre_go_qemu_cache}/git}"
BIN="/usr/local/bin/erplibre_go_qemu_cache"
CONF_DIR="/etc/erplibre_go_qemu_cache"
UNIT="/etc/systemd/system/erplibre-go-qemu-cache.service"
SERVICE_USER="elqcache"
GO_MIN_MAJOR=1
GO_MIN_MINOR=21
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# EL_SRC_DIR passe outre la déduction : le script se retrouve ainsi lançable
# depuis un chemin copié, et une vérification peut appeler UNE de ses
# fonctions sans que la déduction échoue faute de dépôt autour.
SRC_DIR="${EL_SRC_DIR:-$(cd "${SCRIPT_DIR}/../qemu_cache" && pwd)}"
log() { echo "[cache QEMU] $*"; }
die() {
echo "[cache QEMU] ERREUR : $*" >&2
exit 1
}
check_root() {
if [ "$EUID" -ne 0 ]; then
die "à lancer en root : sudo bash $0"
fi
}
detect_os() {
[ -f /etc/os-release ] || die "système inconnu (pas de /etc/os-release)"
# shellcheck disable=SC1091
. /etc/os-release
OS="${ID}"
OS_LIKE="${ID_LIKE:-}"
log "système : ${OS}"
}
is_arch_like() {
case "$OS" in
arch | manjaro | endeavouros | artix | garuda) return 0 ;;
*) echo "$OS_LIKE" | grep -q "arch" && return 0 || return 1 ;;
esac
}
is_debian_like() {
case "$OS" in
ubuntu | debian | linuxmint | pop | elementary | raspbian) return 0 ;;
*) echo "$OS_LIKE" | grep -qE "debian|ubuntu" && return 0 || return 1 ;;
esac
}
is_fedora_like() {
case "$OS" in
fedora | rhel | centos | almalinux | rocky) return 0 ;;
*) echo "$OS_LIKE" | grep -qE "fedora|rhel" && return 0 || return 1 ;;
esac
}
# ---------------------------------------------------------------------------
# Le réseau que les VM utilisent VRAIMENT
# ---------------------------------------------------------------------------
# detecter_reseau remplit EL_BRIDGE et EL_SUBNET depuis libvirt.
#
# Le réseau « default » ne sert pas toujours 192.168.122.0/24 : il est déplacé
# sur un /24 libre quand ce préfixe entre en collision avec ce que l'hôte
# route déjà — le cas de tout orchestrateur qui est lui-même une VM. Poser des
# règles sur le mauvais préfixe laisse une installation qui réussit, des
# règles bien présentes dans le noyau, et un cache que personne ne traverse.
detecter_reseau() {
local xml adresse masque
# Donnés à la main, les deux se suffisent. Une machine dont le réseau
# libvirt n'est pas démarré — ou qui gère son pont autrement — doit pouvoir
# poser le cache en nommant ce pont. Sonder quand même faisait mourir
# l'installation sur une absence qui n'empêchait rien, et le contournement
# annoncé en tête de ce fichier ne servait à rien.
if [ -n "$EL_BRIDGE" ] && [ -n "$EL_SUBNET" ]; then
log "réseau donné à la main : ${EL_BRIDGE}, ${EL_SUBNET}"
return 0
fi
xml="$(virsh -c qemu:///system net-dumpxml "$EL_NET" 2>/dev/null)" || {
die "réseau libvirt « ${EL_NET} » introuvable — virsh -c qemu:///system net-list" \
"le démarrer : sudo virsh -c qemu:///system net-start ${EL_NET}" \
"ou nommer le pont : EL_BRIDGE=virbr0 EL_SUBNET=192.168.122.0/24"
}
[ -n "$EL_BRIDGE" ] || EL_BRIDGE="$(echo "$xml" |
sed -n "s/.*<bridge name='\([^']*\)'.*/\1/p" | head -1)"
if [ -z "$EL_SUBNET" ]; then
adresse="$(echo "$xml" | sed -n "s/.*<ip address='\([^']*\)'.*/\1/p" | head -1)"
masque="$(echo "$xml" | sed -n "s/.*netmask='\([^']*\)'.*/\1/p" | head -1)"
[ "$masque" = "255.255.255.0" ] || die \
"le réseau « ${EL_NET} » ne sert pas un /24 (masque ${masque:-inconnu}) :" \
"passer EL_SUBNET à la main"
EL_SUBNET="${adresse%.*}.0/24"
fi
[ -n "$EL_BRIDGE" ] && [ -n "$EL_SUBNET" ] || die \
"pont ou sous-réseau illisibles dans le réseau « ${EL_NET} »"
log "réseau lu dans libvirt : ${EL_BRIDGE}, ${EL_SUBNET}"
}
# ---------------------------------------------------------------------------
# Go
# ---------------------------------------------------------------------------
# find_go cherche au-delà du PATH de root : un Go posé par mise ou par un
# gestionnaire de versions vit dans le compte de l'opérateur, que sudo ne
# traverse pas.
find_go() {
local candidat
for candidat in \
"$(command -v go 2>/dev/null || true)" \
/usr/local/go/bin/go \
/usr/lib/go/bin/go; do
[ -n "$candidat" ] && [ -x "$candidat" ] && echo "$candidat" && return 0
done
return 1
}
go_assez_recent() {
local go_bin="$1" v major minor
v="$("$go_bin" version 2>/dev/null | sed -n 's/.*go\([0-9]*\)\.\([0-9]*\).*/\1 \2/p')"
[ -z "$v" ] && return 1
major="${v% *}"
minor="${v#* }"
[ "$major" -gt "$GO_MIN_MAJOR" ] && return 0
[ "$major" -eq "$GO_MIN_MAJOR" ] && [ "$minor" -ge "$GO_MIN_MINOR" ] && return 0
return 1
}
installer_go() {
log "Go absent ou trop ancien : installation par le gestionnaire de paquets"
if is_arch_like; then
pacman -S --needed --noconfirm go
elif is_debian_like; then
DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 update -qq
DEBIAN_FRONTEND=noninteractive apt-get -o DPkg::Lock::Timeout=600 \
install -y golang-go
elif is_fedora_like; then
dnf install -y golang
else
die "distribution non prévue : installer Go ${GO_MIN_MAJOR}.${GO_MIN_MINOR}+ à la main"
fi
}
assurer_go() {
local go_bin
if go_bin="$(find_go)" && go_assez_recent "$go_bin"; then
GO="$go_bin"
log "Go : $("$GO" version)"
return
fi
installer_go
go_bin="$(find_go)" || die "Go reste introuvable après installation"
go_assez_recent "$go_bin" ||
die "le Go de la distribution est plus ancien que ${GO_MIN_MAJOR}.${GO_MIN_MINOR} ; poser une version récente à la main"
GO="$go_bin"
log "Go : $("$GO" version)"
}
# ---------------------------------------------------------------------------
# Compilation
# ---------------------------------------------------------------------------
compiler() {
[ -f "${SRC_DIR}/go.mod" ] || die "sources absentes : ${SRC_DIR}"
log "compilation depuis ${SRC_DIR}"
# Le cache de compilation dans un temporaire : root n'a pas à laisser
# d'état dans son compte pour une compilation unique.
local gocache
gocache="$(mktemp -d)"
(
cd "$SRC_DIR"
GOCACHE="$gocache" GOFLAGS=-mod=mod "$GO" build -o "$BIN" .
)
rm -rf "$gocache"
chmod 0755 "$BIN"
log "posé : ${BIN} ($("$BIN" --version))"
}
# ---------------------------------------------------------------------------
# Compte, répertoires, autorité
# ---------------------------------------------------------------------------
preparer_etat() {
if ! id "$SERVICE_USER" >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin "$SERVICE_USER" \
2>/dev/null ||
useradd --system --no-create-home --shell /sbin/nologin "$SERVICE_USER"
log "compte de service créé : ${SERVICE_USER}"
fi
mkdir -p "$EL_CACHE_DIR" "$EL_CA_DIR" "$CONF_DIR" "$EL_GIT_MIRROR_DIR"
touch "$EL_ACCESS_LOG"
# La liste des exceptions appartient à root : elle décide qui échappe au
# cache, et le compte du service ne doit pas pouvoir s'y ajouter.
mkdir -p "$(dirname "$EL_BYPASS_FILE")"
[ -f "$EL_BYPASS_FILE" ] || cat >"$EL_BYPASS_FILE" <<'BYPASS'
# Exceptions du cache de téléchargement des VM QEMU.
# Une ligne « <MAC> <nom de la VM> » par machine que le détournement doit
# ignorer. Relu à chaque démarrage du service.
BYPASS
chmod 0644 "$EL_BYPASS_FILE"
chown -R "${SERVICE_USER}:${SERVICE_USER}" "$EL_CACHE_DIR" "$EL_CA_DIR" "$EL_ACCESS_LOG" "$EL_GIT_MIRROR_DIR"
chmod 0755 "$EL_CACHE_DIR" "$EL_CA_DIR"
}
# L'autorité est créée ICI plutôt qu'au premier démarrage : l'installation
# doit pouvoir en dire l'empreinte, que l'opérateur compare ensuite dans la VM.
creer_autorite() {
runuser -u "$SERVICE_USER" -- "$BIN" --init-ca --ca-dir "$EL_CA_DIR" ||
die "l'autorité n'a pas pu être créée dans ${EL_CA_DIR}"
}
# ---------------------------------------------------------------------------
# Configuration et unité
# ---------------------------------------------------------------------------
ecrire_config() {
cat >"${CONF_DIR}/env" <<CONF
# Réglages du cache de téléchargement des VM QEMU.
# Changer une valeur puis : systemctl restart erplibre-go-qemu-cache
EL_CACHE_DIR=${EL_CACHE_DIR}
EL_CA_DIR=${EL_CA_DIR}
EL_HTTP_PORT=${EL_HTTP_PORT}
EL_TLS_PORT=${EL_TLS_PORT}
EL_BRIDGE=${EL_BRIDGE}
EL_SUBNET=${EL_SUBNET}
EL_ACCESS_LOG=${EL_ACCESS_LOG}
EL_EXCLUDE=${EL_EXCLUDE}
EL_BYPASS_FILE=${EL_BYPASS_FILE}
EL_GIT_MIRROR_DIR=${EL_GIT_MIRROR_DIR}
CONF
chmod 0644 "${CONF_DIR}/env"
}
# Le détournement est choisi une fois, à l'installation : nft s'il est là,
# iptables sinon. Les deux jeux de règles viennent du binaire.
regles_apply_cmd() {
if command -v nft >/dev/null 2>&1; then
echo "${BIN} --print-nft --bridge \${EL_BRIDGE} --subnet \${EL_SUBNET} --http-port \${EL_HTTP_PORT} --tls-port \${EL_TLS_PORT} --bypass-file \${EL_BYPASS_FILE} | nft -f -"
else
echo "${BIN} --print-iptables --bridge \${EL_BRIDGE} --subnet \${EL_SUBNET} --http-port \${EL_HTTP_PORT} --tls-port \${EL_TLS_PORT} --bypass-file \${EL_BYPASS_FILE} | sh -s"
fi
}
regles_clear_cmd() {
if command -v nft >/dev/null 2>&1; then
echo "nft delete table ip erplibre_qemu_cache 2>/dev/null || true"
else
echo "${BIN} --print-iptables --bridge \${EL_BRIDGE} --subnet \${EL_SUBNET} --http-port \${EL_HTTP_PORT} --tls-port \${EL_TLS_PORT} --bypass-file \${EL_BYPASS_FILE} | sed 's/ -A / -D /' | sh -s || true"
fi
}
ecrire_unite() {
# « + » devant une commande la fait tourner en root même sous User= : poser
# une règle de pare-feu demande un privilège que le service n'a pas, et ne
# doit pas avoir, pendant qu'il sert des fichiers.
cat >"$UNIT" <<UNITE
[Unit]
Description=Cache de téléchargement ERPLibre pour les VM QEMU
After=network.target libvirtd.service
Wants=libvirtd.service
[Service]
Type=simple
EnvironmentFile=${CONF_DIR}/env
User=${SERVICE_USER}
Group=${SERVICE_USER}
ExecStartPre=+/bin/sh -c '$(regles_apply_cmd)'
ExecStart=${BIN} --cache-dir \${EL_CACHE_DIR} --ca-dir \${EL_CA_DIR} --http-port \${EL_HTTP_PORT} --tls-port \${EL_TLS_PORT} --bridge \${EL_BRIDGE} --subnet \${EL_SUBNET} --access-log \${EL_ACCESS_LOG} --exclude \${EL_EXCLUDE} --git-mirror-dir \${EL_GIT_MIRROR_DIR}
ExecStopPost=+/bin/sh -c '$(regles_clear_cmd)'
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=${EL_CACHE_DIR} ${EL_CA_DIR} ${EL_ACCESS_LOG} ${EL_GIT_MIRROR_DIR}
Environment=HOME=${EL_CACHE_DIR}
Environment=GIT_CONFIG_GLOBAL=/dev/null
Environment=GIT_TERMINAL_PROMPT=0
[Install]
WantedBy=multi-user.target
UNITE
chmod 0644 "$UNIT"
systemctl daemon-reload
}
# ---------------------------------------------------------------------------
main() {
check_root
detect_os
detecter_reseau
assurer_go
compiler
preparer_etat
creer_autorite
ecrire_config
ecrire_unite
systemctl enable erplibre-go-qemu-cache.service >/dev/null 2>&1 || true
systemctl restart erplibre-go-qemu-cache.service
# Un service qui ne tient pas doit le dire ici, et non se découvrir au
# prochain déploiement de VM.
sleep 2
if ! systemctl is-active --quiet erplibre-go-qemu-cache.service; then
systemctl status --no-pager -l erplibre-go-qemu-cache.service || true
die "le service ne tient pas ; les règles ont été retirées à son arrêt"
fi
log ""
log "cache actif sur ${EL_HTTP_PORT} (http) et ${EL_TLS_PORT} (tls)"
log "détournement : ${EL_BRIDGE}, depuis ${EL_SUBNET}, sortie du /24 seule"
log "autorité à faire approuver dans chaque VM :"
log " ${EL_CA_DIR}/ca.crt"
"$BIN" --status --cache-dir "$EL_CACHE_DIR" --ca-dir "$EL_CA_DIR" \
--git-mirror-dir "$EL_GIT_MIRROR_DIR" || true
}
main "$@"