erplibre/script/install/install_qemu_cache.sh

422 lines
16 KiB
Bash
Raw Normal View History

#!/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:-}"
# Langue des messages du service : « fr » ou « en », comme EL_LANG de todo.py.
EL_LANG="${EL_LANG:-fr}"
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="${EL_CONF_DIR:-/etc/erplibre_go_qemu_cache}"
PURGE_UNIT="/etc/systemd/system/erplibre-go-qemu-cache-purge.service"
PURGE_TIMER="/etc/systemd/system/erplibre-go-qemu-cache-purge.timer"
# Les réglages de nettoyage choisis depuis le menu survivent à une
# réinstallation : le fichier env est réécrit en entier, et les retrouver vides
# changerait en silence le comportement choisi. Une valeur passée dans
# l'environnement, même vide, l'emporte — c'est ainsi qu'on les désactive.
reglage_existant() {
sed -n "s/^$1=//p" "${CONF_DIR}/env" 2>/dev/null | head -n 1
}
EL_PURGE_AGE="${EL_PURGE_AGE-$(reglage_existant EL_PURGE_AGE)}"
EL_MAX_SIZE="${EL_MAX_SIZE-$(reglage_existant EL_MAX_SIZE)}"
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}
EL_LANG=${EL_LANG}
# Nettoyage automatique, chaque jour ; vide = désactivé. Réglable depuis le
# menu du cache de todo.py, sans réinstaller.
# EL_PURGE_AGE : ce qui n'a pas servi depuis ce délai part (ex. 90j).
# EL_MAX_SIZE : au-delà, le moins récemment servi part (ex. 50G).
EL_PURGE_AGE=${EL_PURGE_AGE}
EL_MAX_SIZE=${EL_MAX_SIZE}
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
}
ecrire_purge() {
# Un minuteur quotidien qui ne fait RIEN tant que les deux réglages sont
# vides : le nettoyage reste manuel par défaut, et le régler suffit à
# l'activer, sans réinstaller. Sous le compte du service, propriétaire du
# magasin : un effacement en root laisserait des répertoires que le service
# ne pourrait plus écrire. « $$ » rend un « $ » au shell : la variable vient
# de l'EnvironmentFile au moment de l'exécution, jamais de ce script.
cat >"$PURGE_UNIT" <<UNITE
[Unit]
Description=Nettoyage automatique du cache de téléchargement ERPLibre
After=erplibre-go-qemu-cache.service
[Service]
Type=oneshot
EnvironmentFile=${CONF_DIR}/env
User=${SERVICE_USER}
Group=${SERVICE_USER}
ExecStart=/bin/sh -c 'if [ -n "\$\$EL_PURGE_AGE" ]; then ${BIN} --cache-dir "\$\$EL_CACHE_DIR" --git-mirror-dir "\$\$EL_GIT_MIRROR_DIR" --lang "\$\$EL_LANG" --purge-older-than "\$\$EL_PURGE_AGE"; fi; if [ -n "\$\$EL_MAX_SIZE" ]; then ${BIN} --cache-dir "\$\$EL_CACHE_DIR" --git-mirror-dir "\$\$EL_GIT_MIRROR_DIR" --lang "\$\$EL_LANG" --purge-to-size "\$\$EL_MAX_SIZE"; fi'
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=${EL_CACHE_DIR} ${EL_GIT_MIRROR_DIR}
UNITE
cat >"$PURGE_TIMER" <<MINUTEUR
[Unit]
Description=Nettoyage quotidien du cache de téléchargement ERPLibre
[Timer]
OnCalendar=daily
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target
MINUTEUR
chmod 0644 "$PURGE_UNIT" "$PURGE_TIMER"
systemctl daemon-reload
}
# ---------------------------------------------------------------------------
main() {
check_root
detect_os
detecter_reseau
assurer_go
compiler
preparer_etat
creer_autorite
ecrire_config
ecrire_unite
ecrire_purge
systemctl enable --now erplibre-go-qemu-cache-purge.timer >/dev/null 2>&1 || true
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 "$@"