Trois pannes trouvées en LANÇANT la descente, aucune vue en la relisant — ni
par moi, ni par l'attaque adversariale.
Le premier « apt update » d'une image cloud échoue sur un verrou qui n'est pas
celui qu'on croit. Mesuré une seconde après le premier ssh :
E: Could not get lock /var/lib/apt/lists/lock.
It is held by process 1026 (apt-get)
Ce n'est pas cloud-init — « status --wait » avait rendu la main. C'est
apt-daily, le minuteur de Debian, qui se déclenche au démarrage. Et le verrou
des LISTES n'est pas couvert par « DPkg::Lock::Timeout », que l'installeur
réglait pourtant déjà à 600 s : cette attente ne vaut que pour dpkg. On arrête
donc les minuteurs, puis on RÉESSAIE — arrêter une unité n'interrompt pas
l'apt-get déjà en vol. Le défaut touchait tout déploiement Proxmox, pas
seulement ce test.
Le premier étage n'avait pas d'alias ssh. « deploy_qemu.py » en ligne de
commande n'écrit pas d'entrée ~/.ssh/config — le menu le fait, la CLI non. La
descente aurait attendu son plein délai avant de conclure « jamais joignable »
sur une VM qui répondait à son adresse. Elle l'écrit maintenant elle-même,
depuis l'adresse résolue, et refuse d'avancer si la VM n'en a pas.
Et la réserve de l'hôte est proportionnelle. Quatre gigaoctets sur une machine
de soixante, c'était 6 % laissés au système : le jour où les invités touchent
vraiment leur mémoire, c'est l'hôte qui part en swap — et la mesure serait
celle du swap, pas de l'imbrication. Un huitième, avec le plancher d'avant
pour les petites machines.
Ce que la descente a établi en trois étages : 392 s, 644 s, 1120 s, soit 1,7
fois par étage. Puis la poignée de main ssh passe de 77 à 1664 secondes au
quatrième — vingt fois d'un seul cran. Le coude est là.
Et le mur que j'avais pris pour une limite d'imbrication n'en était pas une.
La VM qui gelait au quatrième étage avait douze vCPU ; celle-ci en a deux et
elle passe, en écrivant. C'était une limite de parallélisme SOUS imbrication —
exactement ce que l'algorithme borne, vérifié pour la première fois plutôt que
supposé.
--- EN ---
Three faults found by RUNNING the descent, none seen by reading it — neither
by me nor by the adversarial attack.
A cloud image's first "apt update" fails on a lock that is not the one you
expect. Measured one second after the first ssh:
E: Could not get lock /var/lib/apt/lists/lock.
It is held by process 1026 (apt-get)
It is not cloud-init — "status --wait" had returned. It is apt-daily, Debian's
timer, firing at boot. And the LISTS lock is not covered by
"DPkg::Lock::Timeout", which the installer already set to 600 s: that wait
only applies to dpkg. So we stop the timers, then RETRY — stopping a unit does
not interrupt the apt-get already in flight. The defect affected every Proxmox
deployment, not just this test.
The first level had no ssh alias. "deploy_qemu.py" on the command line does not
write a ~/.ssh/config entry — the menu does, the CLI does not. The descent
would have waited its full timeout before concluding "never reachable" about a
VM answering at its address. It now writes the entry itself, from the resolved
address, and refuses to proceed if the VM has none.
And the host's reserve is proportional. Four gigabytes on a sixty-gigabyte
machine left 6 % to the system: the day the guests really touch their memory,
the host swaps — and the measurement would be of swap, not of nesting. One
eighth now, keeping the old floor for small machines.
What the descent established over three levels: 392 s, 644 s, 1120 s — 1.7x per
level. Then the ssh handshake goes from 77 to 1664 seconds at the fourth:
twenty times in one step. That is the elbow.
And the wall I had taken for a nesting limit was not one. The VM that froze at
the fourth level had twelve vCPU; this one has two and it gets through,
writing. It was a limit of parallelism UNDER nesting — exactly what the
algorithm caps, verified for the first time rather than assumed.
Assisted-by: Claude Opus 5
(cherry picked from commit 7f86562cbd8f10017dcb88fe4272efc162cbccbc)
635 lines
28 KiB
Bash
Executable file
635 lines
28 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)
|
|
#
|
|
# Installe Proxmox VE — https://www.proxmox.com — SUR UNE DEBIAN existante.
|
|
#
|
|
# Pourquoi sur Debian : Proxmox ne publie aucune image cloud. Son ISO est un
|
|
# installateur qui formate le disque, ce qui ne se pilote pas depuis un
|
|
# déploiement cloud-init. La voie que l'amont documente lui-même pour ce cas
|
|
# est « Install Proxmox VE on Debian » : on part de l'image cloud Debian, on
|
|
# ajoute le dépôt pve, et les paquets font le reste. Le résultat est le même
|
|
# hyperviseur, avec le noyau Proxmox et l'interface web sur :8006.
|
|
#
|
|
# Architectures : amd64 et arm64 (arm64 officiel depuis PVE 9 — le Release de
|
|
# trixie annonce « amd64 arm64 »). PAS s390x : l'index « binary-s390x » du
|
|
# dépôt répond 404, le port n'existe pas.
|
|
#
|
|
# Réglages, par variables d'environnement :
|
|
# PVE_SUITE suite Debian visée (défaut : trixie, = PVE 9)
|
|
# PVE_REBOOT à 1, redémarre à la fin (défaut : ne redémarre PAS —
|
|
# lancé par SSH, un reboot couperait la session et ferait
|
|
# passer une installation réussie pour un échec)
|
|
# PVE_KEEP_DEBIAN_KERNEL à 1, garde le noyau Debian à côté du noyau pve
|
|
# PVE_OS_RELEASE fichier os-release à lire (défaut : /etc/os-release) ;
|
|
# sert à vérifier le script depuis une autre distribution
|
|
set -euo pipefail
|
|
|
|
Red='\033[0;31m'
|
|
Green='\033[0;32m'
|
|
Yellow='\033[0;33m'
|
|
Color_Off='\033[0m'
|
|
|
|
SUITE="${PVE_SUITE:-trixie}"
|
|
KEYRING=/usr/share/keyrings/proxmox-archive-keyring.gpg
|
|
SOURCES=/etc/apt/sources.list.d/pve-install-repo.sources
|
|
|
|
# Somme relevée sur la page amont « Install Proxmox VE on Debian 13 Trixie »,
|
|
# et VÉRIFIÉE contre le fichier servi. Elle vaut ceinture ET bretelle : c'est
|
|
# la clé qui authentifiera tout le reste, et la télécharger sans la contrôler
|
|
# reviendrait à faire confiance au seul transport.
|
|
KEY_URL="https://enterprise.proxmox.com/debian/proxmox-archive-keyring-${SUITE}.gpg"
|
|
KEY_SHA256_trixie=136673be77aba35dcce385b28737689ad64fd785a797e57897589aed08db6e45
|
|
|
|
# Ce qui a changé : décide du redémarrage final. Une installation déjà faite ne
|
|
# doit pas redémarrer la machine pour rien.
|
|
CHANGED=0
|
|
|
|
say() { echo -e "$@"; }
|
|
die() { say "${Red}✗${Color_Off} $*"; exit 1; }
|
|
|
|
DRY=0
|
|
usage() {
|
|
cat <<EOF
|
|
Installe Proxmox VE sur une Debian existante (amd64, arm64).
|
|
|
|
--dry-run dit ce qu'il ferait, sans rien changer
|
|
--help cette aide
|
|
|
|
Variables : PVE_SUITE (défaut trixie), PVE_REBOOT, PVE_KEEP_DEBIAN_KERNEL,
|
|
PVE_OS_RELEASE
|
|
EOF
|
|
}
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--dry-run) DRY=1 ;;
|
|
-h | --help) usage; exit 0 ;;
|
|
*) die "option inconnue : $1" ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# Toute action qui MODIFIE la machine passe par ici. En dry-run elle est
|
|
# annoncée, pas exécutée : c'est ce qui rend le script vérifiable de bout en
|
|
# bout sans hyperviseur ni dépôt à disposition.
|
|
run() {
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} $*"
|
|
return 0
|
|
fi
|
|
"$@"
|
|
}
|
|
|
|
# --- 1. Architecture --------------------------------------------------------
|
|
case "$(uname -m)" in
|
|
x86_64) ARCH=amd64 ;;
|
|
aarch64 | arm64) ARCH=arm64 ;;
|
|
*)
|
|
die "Proxmox VE n'existe pas pour $(uname -m) :" \
|
|
"le dépôt ne publie que amd64 et arm64."
|
|
;;
|
|
esac
|
|
|
|
# --- 2. Debian, et la bonne suite ------------------------------------------
|
|
OS_RELEASE="${PVE_OS_RELEASE:-/etc/os-release}"
|
|
[ -r "${OS_RELEASE}" ] || die "${OS_RELEASE} illisible : Debian attendue."
|
|
# shellcheck disable=SC1090
|
|
. "${OS_RELEASE}"
|
|
if [ "${ID:-}" != "debian" ]; then
|
|
die "Proxmox VE s'installe sur Debian, pas sur « ${ID:-inconnu} »." \
|
|
"\n Redéployer la VM avec --distro proxmox (base Debian ${SUITE})."
|
|
fi
|
|
CODENAME="${VERSION_CODENAME:-}"
|
|
if [ -n "${CODENAME}" ] && [ "${CODENAME}" != "${SUITE}" ]; then
|
|
say "${Yellow}⚠${Color_Off} Debian « ${CODENAME} » alors que le dépôt visé" \
|
|
"est « ${SUITE} » : PVE_SUITE=${CODENAME} si c'est voulu."
|
|
fi
|
|
|
|
# --- 3. /etc/hosts : le nom d'hôte doit résoudre vers une IP ROUTABLE -------
|
|
# Ce n'est pas un détail de confort. Le système de grappe de Proxmox parcourt
|
|
# toutes les adresses du nom d'hôte jusqu'à en trouver une qui ne soit pas de
|
|
# bouclage ; l'entrée « 127.0.1.1 <nom> » que pose l'image cloud Debian le
|
|
# mène droit dans le mur, et pveproxy comme pvecm s'en trouvent mal.
|
|
# « hostname --ip-address » est le test que l'amont donne lui-même.
|
|
host_ip() {
|
|
local ip=""
|
|
for ip in \
|
|
"$(ip -4 route get 1 2>/dev/null | awk '{print $7; exit}')" \
|
|
"$(hostname -I 2>/dev/null | awk '{print $1}')" \
|
|
"$(ip -4 -o addr show scope global 2>/dev/null \
|
|
| awk '{split($4, a, "/"); print a[1]; exit}')"
|
|
do
|
|
case "$ip" in
|
|
127.*) continue ;;
|
|
[0-9]*.[0-9]*.[0-9]*.[0-9]*) echo "$ip"; return 0 ;;
|
|
esac
|
|
done
|
|
return 1
|
|
}
|
|
|
|
# Sans ceci, tout ce que fait fix_hosts est ANNULÉ au prochain démarrage.
|
|
# L'image cloud Debian règle « manage_etc_hosts: True » : cloud-init réécrit
|
|
# alors /etc/hosts depuis son gabarit à chaque boot, et y remet
|
|
# « 127.0.1.1 <nom> ». pmxcfs, qui cherche une adresse non-bouclage pour le
|
|
# nom d'hôte, ne démarre plus — /etc/pve n'est pas monté, « pvesm » répond
|
|
# « Connection refused », et l'écran de déploiement conclut « il manque le
|
|
# stockage ». Le vrai défaut est trois étages plus bas.
|
|
#
|
|
# Vécu, et révélé par le redémarrage désormais automatique : l'installation
|
|
# corrigeait /etc/hosts, le reboot amorçait le noyau Proxmox, et cloud-init
|
|
# défaisait la correction dans le même mouvement.
|
|
#
|
|
# Un fichier de surcharge plutôt qu'une édition de cloud.cfg : c'est la voie
|
|
# que cloud-init documente, et une mise à jour du paquet ne l'écrase pas.
|
|
freeze_cloud_hosts() {
|
|
local dossier=/etc/cloud/cloud.cfg.d
|
|
local fichier="${dossier}/99-erplibre-hosts.cfg"
|
|
[ -d /etc/cloud ] || return 0
|
|
# Sur le CONTENU et non sur l'existence : « printf … > fichier » TRONQUE
|
|
# avant d'écrire. Une coupure au mauvais moment laisse un fichier de zéro
|
|
# octet, et une garde à l'existence annonce alors « déjà gelé » pour
|
|
# toujours — cloud-init continue de remettre 127.0.1.1 à chaque
|
|
# démarrage, et le défaut redevient invisible. Une redirection est de
|
|
# toute façon idempotente : il n'y a rien à protéger d'autre.
|
|
if grep -qE "^[[:space:]]*manage_etc_hosts:[[:space:]]*false" \
|
|
"${fichier}" 2>/dev/null; then
|
|
say " cloud-init ne touche déjà plus à /etc/hosts"
|
|
return 0
|
|
fi
|
|
say " cloud-init : gel de /etc/hosts (${fichier})"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} manage_etc_hosts: false" \
|
|
"> ${fichier}"
|
|
return 0
|
|
fi
|
|
sudo mkdir -p "${dossier}"
|
|
printf '%s\n' \
|
|
"# Posé par ERPLibre : Proxmox exige que le nom d'hôte résolve vers" \
|
|
"# une adresse ROUTABLE. cloud-init y remettait 127.0.1.1 à chaque" \
|
|
"# démarrage, et pmxcfs ne démarrait plus." \
|
|
"manage_etc_hosts: false" \
|
|
| sudo tee "${fichier}" >/dev/null
|
|
CHANGED=1
|
|
}
|
|
|
|
fix_hosts() {
|
|
local ip fqdn short
|
|
ip="$(host_ip)" || die "aucune adresse IPv4 routable : réseau absent ?"
|
|
freeze_cloud_hosts
|
|
short="$(hostname -s)"
|
|
fqdn="$(hostname -f 2>/dev/null || echo "${short}")"
|
|
[ "${fqdn}" = "${short}" ] && fqdn="${short}.local"
|
|
if grep -qE "^[[:space:]]*127\.0\.1\.1[[:space:]]" /etc/hosts; then
|
|
say " retrait de l'entrée 127.0.1.1 (bouclage) pour ${short}"
|
|
run sudo sed -i -E "/^[[:space:]]*127\.0\.1\.1[[:space:]]/d" \
|
|
/etc/hosts
|
|
CHANGED=1
|
|
fi
|
|
if ! grep -qE "^[[:space:]]*${ip//./\\.}[[:space:]]+.*\b${short}\b" \
|
|
/etc/hosts; then
|
|
say " ${short} -> ${ip} dans /etc/hosts"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} ${ip} ${fqdn} ${short}" \
|
|
">> /etc/hosts"
|
|
else
|
|
printf '%s\t%s %s\n' "${ip}" "${fqdn}" "${short}" \
|
|
| sudo tee -a /etc/hosts >/dev/null
|
|
fi
|
|
CHANGED=1
|
|
fi
|
|
# Le test de l'amont, mot pour mot : au moins une adresse non-bouclage.
|
|
local vu routables
|
|
vu="$(hostname --ip-address 2>/dev/null || true)"
|
|
routables="$(printf '%s\n' ${vu} | grep -vE '^(127\.|::1$)' || true)"
|
|
[ -n "${routables}" ] || die \
|
|
"« hostname --ip-address » rend « ${vu:-rien} » : le nom d'hôte ne" \
|
|
"résout toujours pas vers une adresse routable."
|
|
say " hostname --ip-address : $(printf '%s ' ${routables})"
|
|
revive_pve_services
|
|
}
|
|
|
|
# Les services de Proxmox abandonnent après cinq essais rapprochés : systemd
|
|
# marque l'unité « failed » et n'y revient JAMAIS de lui-même — « Start request
|
|
# repeated too quickly ». Or ils ont TOUS échoué pendant que /etc/hosts était
|
|
# faux. Corriger le fichier ne suffit donc pas.
|
|
#
|
|
# L'ordre compte : pve-cluster d'abord, il monte /etc/pve dont les autres
|
|
# dépendent.
|
|
#
|
|
# pvestatd n'est pas un luxe. C'est lui qui remplit « /cluster/resources » ;
|
|
# arrêté, l'hôte rend une entrée SQUELETTIQUE par VM — ni nom, ni mémoire, ni
|
|
# disque, et « status: unknown ». Le tableau de bord n'a alors aucune colonne
|
|
# vivante, et il a même pris cette entrée pour une VM disparue.
|
|
# pve-firewall n'y est PAS, et c'est délibéré. Sa configuration vit dans
|
|
# /var/lib/pve-cluster/config.db, donc elle est invisible tant que /etc/pve
|
|
# n'est pas monté — c'est-à-dire exactement dans l'état qu'on répare. Le
|
|
# démarrer, c'est appliquer des règles qu'on ne peut pas lire sur la seule
|
|
# voie d'accès à la machine : ce script tourne au bout d'un ssh, et une VM
|
|
# imbriquée n'a pas d'autre porte. Une révision adversariale l'a classé
|
|
# « isole l'hôte » par trois lentilles indépendantes.
|
|
#
|
|
# Il n'est de toute façon pas nécessaire au but : le stockage et le suivi
|
|
# demandent pve-cluster et pvestatd, l'interface web pveproxy. Le pare-feu
|
|
# repartira au prochain démarrage, quand /etc/pve sera monté à temps.
|
|
PVE_SERVICES="pve-cluster pvestatd pvedaemon pveproxy"
|
|
|
|
revive_pve_services() {
|
|
command -v systemctl >/dev/null 2>&1 || return 0
|
|
local unite etat casse=""
|
|
for unite in ${PVE_SERVICES}; do
|
|
systemctl list-unit-files "${unite}.service" >/dev/null 2>&1 || continue
|
|
etat="$(systemctl is-active "${unite}" 2>/dev/null || true)"
|
|
[ "${etat}" = "active" ] && continue
|
|
casse="${casse} ${unite}"
|
|
done
|
|
[ -n "${casse}" ] || return 0
|
|
say " services à relancer :${casse}"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} systemctl reset-failed puis" \
|
|
"start :${casse}"
|
|
return 0
|
|
fi
|
|
for unite in ${casse}; do
|
|
sudo systemctl reset-failed "${unite}" 2>/dev/null || true
|
|
sudo systemctl start "${unite}" 2>&1 || \
|
|
say " ${Yellow}⚠${Color_Off} ${unite} :" \
|
|
"journalctl -u ${unite} -n 30"
|
|
CHANGED=1
|
|
done
|
|
# Le montage n'est pas instantané : on le CONSTATE plutôt que de le
|
|
# supposer, et on le dit quand il n'arrive pas.
|
|
local i
|
|
for i in 1 2 3 4 5 6 7 8 9 10; do
|
|
[ -e /etc/pve/.version ] && break
|
|
sleep 1
|
|
done
|
|
if [ -e /etc/pve/.version ]; then
|
|
say " ${Green}✓${Color_Off} /etc/pve monté"
|
|
else
|
|
say " ${Yellow}⚠${Color_Off} /etc/pve toujours absent :" \
|
|
"journalctl -u pve-cluster -n 30"
|
|
fi
|
|
}
|
|
|
|
# --- 4. Dépôt et clé --------------------------------------------------------
|
|
add_repo() {
|
|
local attendu="KEY_SHA256_${SUITE}"
|
|
attendu="${!attendu:-}"
|
|
if [ ! -s "${KEYRING}" ]; then
|
|
say " clé du dépôt -> ${KEYRING}"
|
|
run sudo mkdir -p "$(dirname "${KEYRING}")"
|
|
run sudo wget -q "${KEY_URL}" -O "${KEYRING}" \
|
|
|| die "téléchargement de la clé impossible : ${KEY_URL}"
|
|
# Un wget qui rend 0 sans laisser de fichier — proxy captif, disque
|
|
# plein — ferait mourir la suite sur un message de sha256sum au lieu
|
|
# d'un diagnostic.
|
|
if [ "${DRY}" != "1" ]; then
|
|
sudo test -s "${KEYRING}" \
|
|
|| die "clé absente ou vide après téléchargement : ${KEYRING}"
|
|
fi
|
|
CHANGED=1
|
|
fi
|
|
if [ -n "${attendu}" ] && [ "${DRY}" = "1" ]; then
|
|
say " clé à vérifier contre sha256 ${attendu:0:16}…"
|
|
elif [ -n "${attendu}" ]; then
|
|
local vu
|
|
vu="$(sudo sha256sum "${KEYRING}" 2>/dev/null | awk '{print $1}')" \
|
|
|| true
|
|
[ -n "${vu}" ] || die "somme de la clé illisible : ${KEYRING}"
|
|
if [ "${vu}" != "${attendu}" ]; then
|
|
# On efface la clé douteuse : la laisser en place ferait passer la
|
|
# prochaine exécution pour bonne, le fichier étant non vide.
|
|
run sudo rm -f "${KEYRING}"
|
|
die "somme de la clé inattendue :\n vue ${vu}\n" \
|
|
" attendue ${attendu}"
|
|
fi
|
|
say " clé vérifiée (sha256 ${vu:0:16}…)"
|
|
else
|
|
say "${Yellow}⚠${Color_Off} aucune somme connue pour « ${SUITE} » :" \
|
|
"clé acceptée sans contrôle."
|
|
fi
|
|
# Format deb822, celui que l'amont recommande sur Debian 13.
|
|
local voulu
|
|
voulu="$(printf 'Types: deb\nURIs: http://download.proxmox.com/debian/pve\nSuites: %s\nComponents: pve-no-subscription\nSigned-By: %s\n' \
|
|
"${SUITE}" "${KEYRING}")"
|
|
if [ "$(cat "${SOURCES}" 2>/dev/null || true)" != "${voulu}" ]; then
|
|
say " dépôt pve-no-subscription -> ${SOURCES}"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} ${SOURCES} :" \
|
|
"$(printf '%s' "${voulu}" | tr '\n' '|')"
|
|
else
|
|
printf '%s\n' "${voulu}" | sudo tee "${SOURCES}" >/dev/null
|
|
fi
|
|
CHANGED=1
|
|
fi
|
|
}
|
|
|
|
# --- 6. Dépôt entreprise ----------------------------------------------------
|
|
# « proxmox-ve » installe SON dépôt entreprise (pve-enterprise.sources), qui
|
|
# exige un abonnement payant. Sans lui, tout « apt update » ultérieur échoue :
|
|
# « 401 Unauthorized » puis « The repository is not signed », et la machine ne
|
|
# peut plus rien installer — pas même une mise à jour de sécurité. Mesuré sur
|
|
# la VM d'essai, au deuxième passage du script.
|
|
#
|
|
# On le désactive au lieu de l'effacer : « Enabled: false » est la forme deb822
|
|
# prévue pour cela, elle survit aux mises à jour du paquet, et il suffit de
|
|
# retirer la ligne le jour où un abonnement existe.
|
|
disable_enterprise() {
|
|
local f
|
|
for f in /etc/apt/sources.list.d/pve-enterprise.sources \
|
|
/etc/apt/sources.list.d/ceph.sources; do
|
|
[ -e "${f}" ] || continue
|
|
if sudo grep -qiE '^[[:space:]]*Enabled:[[:space:]]*(false|no)' \
|
|
"${f}"; then
|
|
continue
|
|
fi
|
|
say " dépôt entreprise désactivé : $(basename "${f}")"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} Enabled: false >> ${f}"
|
|
else
|
|
printf 'Enabled: false\n' | sudo tee -a "${f}" >/dev/null
|
|
fi
|
|
CHANGED=1
|
|
done
|
|
# Les anciennes formes « .list », au cas où une mise à jour les remette.
|
|
for f in /etc/apt/sources.list.d/pve-enterprise.list; do
|
|
[ -e "${f}" ] || continue
|
|
sudo grep -qE '^[[:space:]]*#' "${f}" && continue
|
|
say " dépôt entreprise commenté : $(basename "${f}")"
|
|
run sudo sed -i -E 's/^([[:space:]]*deb)/#\1/' "${f}"
|
|
CHANGED=1
|
|
done
|
|
}
|
|
|
|
# --- 5. Les paquets ---------------------------------------------------------
|
|
# Disque d'amorçage, pour la préréponse de grub-pc.
|
|
boot_disk() {
|
|
local src
|
|
src="$(findmnt -no SOURCE /boot 2>/dev/null \
|
|
|| findmnt -no SOURCE / 2>/dev/null)"
|
|
[ -n "${src}" ] || return 1
|
|
local dq
|
|
dq="$(lsblk -no pkname "${src}" 2>/dev/null | head -1)"
|
|
[ -n "${dq}" ] || return 1
|
|
printf '/dev/%s\n' "${dq}"
|
|
}
|
|
|
|
# Deux paquets posent des questions debconf, et une seule sans réponse suffit à
|
|
# faire échouer TOUTE la transaction apt :
|
|
#
|
|
# - postfix demande son type et son nom de courrier. Sans préréponse,
|
|
# l'installation attend une saisie que personne ne verra — un déploiement
|
|
# par SSH y reste pendu.
|
|
# - grub-pc demande SUR QUEL DISQUE s'installer. C'est le piège propre à
|
|
# l'image cloud : elle amorce en EFI (grub-cloud-amd64), les paquets pve
|
|
# tirent grub-pc par-dessus, et sa post-installation refuse de deviner —
|
|
# « You must correct your GRUB install devices before proceeding », mesuré,
|
|
# dpkg s'arrête et le noyau Proxmox reste à moitié configuré.
|
|
preseed_debconf() {
|
|
command -v debconf-set-selections >/dev/null 2>&1 || return 0
|
|
local lignes disque
|
|
lignes="$(printf '%s\n' \
|
|
"postfix postfix/main_mailer_type select Local only" \
|
|
"postfix postfix/mailname string $(hostname -f 2>/dev/null \
|
|
|| hostname -s)")"
|
|
if disque="$(boot_disk)"; then
|
|
lignes="$(printf '%s\n%s\n' "${lignes}" \
|
|
"grub-pc grub-pc/install_devices multiselect ${disque}")"
|
|
else
|
|
say "${Yellow}⚠${Color_Off} disque d'amorçage introuvable :" \
|
|
"grub-pc pourrait demander où s'installer."
|
|
fi
|
|
# Pas de tube vers « run » : en dry-run il ne lirait pas son entrée, le
|
|
# printf recevrait SIGPIPE, et « set -o pipefail » emporterait le script —
|
|
# mort silencieuse sur un code 141. Vécu.
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} debconf-set-selections :" \
|
|
"$(printf '%s' "${lignes}" | tr '\n' '|')"
|
|
return 0
|
|
fi
|
|
printf '%s\n' "${lignes}" | sudo debconf-set-selections
|
|
}
|
|
|
|
# « DPkg::Lock::Timeout » : sur une VM fraîchement déployée, cloud-init tient
|
|
# encore le verrou d'apt — mesuré, « E: Could not get lock
|
|
# /var/lib/apt/lists/lock. It is held by process 996 (apt-get) », et le script
|
|
# mourait 40 secondes après le démarrage. apt sait ATTENDRE son tour depuis la
|
|
# 1.9 ; sans cette option il abandonne immédiatement.
|
|
apt_get() {
|
|
run sudo env DEBIAN_FRONTEND=noninteractive \
|
|
apt-get -o Dpkg::Options::=--force-confold \
|
|
-o DPkg::Lock::Timeout=600 -y "$@"
|
|
}
|
|
|
|
# Attendre la fin de cloud-init AVANT de toucher à apt : c'est lui qui pose les
|
|
# paquets de la première mise en route. Son état final n'est PAS un critère —
|
|
# vérifié sur l'image Debian 13, où il rend « error » pour deux modules sans
|
|
# rapport (console-setup absent, update-locale) tout en ayant terminé son
|
|
# travail. On attend qu'il ait fini, pas qu'il soit content.
|
|
wait_cloud_init() {
|
|
command -v cloud-init >/dev/null 2>&1 || return 0
|
|
say " attente de la fin de cloud-init…"
|
|
if [ "${DRY}" = "1" ]; then
|
|
say " ${Yellow}[dry-run]${Color_Off} cloud-init status --wait"
|
|
return 0
|
|
fi
|
|
timeout 600 cloud-init status --wait >/dev/null 2>&1 || true
|
|
return 0
|
|
}
|
|
|
|
# Le premier « apt update » d'une image cloud tombe sur un verrou qui n'est
|
|
# pas celui qu'on croit. Mesuré, une seconde après le premier ssh :
|
|
#
|
|
# E: Could not get lock /var/lib/apt/lists/lock.
|
|
# It is held by process 1026 (apt-get)
|
|
#
|
|
# Ce n'est pas cloud-init — « cloud-init status --wait » avait rendu la main.
|
|
# C'est apt-daily, le minuteur de Debian, qui se déclenche au démarrage. Et le
|
|
# verrou des LISTES n'est pas couvert par « DPkg::Lock::Timeout », qui ne vaut
|
|
# que pour celui de dpkg : l'attente configurée ne s'applique donc pas ici.
|
|
#
|
|
# On arrête les minuteurs, puis on RÉESSAIE — arrêter une unité n'interrompt
|
|
# pas l'apt-get déjà en vol, et cloud-init peut en avoir un autre en route.
|
|
prepare_apt() {
|
|
run sudo systemctl stop apt-daily.service apt-daily-upgrade.service \
|
|
apt-daily.timer apt-daily-upgrade.timer >/dev/null 2>&1 || true
|
|
local i
|
|
for i in $(seq 1 12); do
|
|
if apt_get update; then
|
|
return 0
|
|
fi
|
|
say " verrou apt tenu, nouvel essai dans 15 s (${i}/12)"
|
|
sleep 15
|
|
done
|
|
die "apt update impossible : le verrou des listes reste tenu."
|
|
}
|
|
|
|
install_pve() {
|
|
wait_cloud_init
|
|
preseed_debconf
|
|
# Réparer une transaction laissée à moitié par une exécution précédente :
|
|
# sans réponse à grub-pc, dpkg s'arrête au milieu et tout apt suivant
|
|
# refuse de travailler. Inoffensif quand rien n'est cassé.
|
|
if ! sudo dpkg -C >/dev/null 2>&1; then
|
|
say " paquets à moitié configurés : dpkg --configure -a"
|
|
run sudo env DEBIAN_FRONTEND=noninteractive dpkg --configure -a \
|
|
|| true
|
|
fi
|
|
# AVANT le premier apt : sur un second passage, le dépôt entreprise posé
|
|
# par proxmox-ve ferait échouer « apt update » (401) et rien n'irait plus
|
|
# loin — pas même la désactivation, si elle attendait la fin.
|
|
disable_enterprise
|
|
say "\n---- apt update ----"
|
|
prepare_apt
|
|
# Le noyau d'abord, comme l'amont le prescrit : c'est lui qui porte les
|
|
# modules dont pve a besoin, et l'installer seul laisse une machine qui
|
|
# redémarre proprement même si la suite échoue.
|
|
if ! dpkg -s proxmox-default-kernel >/dev/null 2>&1; then
|
|
say "\n---- noyau Proxmox ----"
|
|
apt_get install proxmox-default-kernel
|
|
CHANGED=1
|
|
else
|
|
say " noyau Proxmox déjà posé"
|
|
fi
|
|
if ! dpkg -s proxmox-ve >/dev/null 2>&1; then
|
|
say "\n---- proxmox-ve, postfix, open-iscsi, chrony ----"
|
|
apt_get install proxmox-ve postfix open-iscsi chrony
|
|
CHANGED=1
|
|
# C'est CETTE installation qui vient de poser le dépôt entreprise :
|
|
# le désactiver tout de suite, avant que le ménage ne rappelle apt.
|
|
disable_enterprise
|
|
else
|
|
say " proxmox-ve déjà posé"
|
|
fi
|
|
}
|
|
|
|
# --- 6. Ménage --------------------------------------------------------------
|
|
# os-prober ajoute au menu d'amorçage les systèmes trouvés sur les disques des
|
|
# VM invitées : sur un hyperviseur, c'est une liste de faux départs.
|
|
# Le noyau Debian, lui, n'a plus de raison d'être une fois celui de pve en
|
|
# place — et laissé par défaut, grub peut y revenir.
|
|
cleanup() {
|
|
if dpkg -s os-prober >/dev/null 2>&1; then
|
|
say " retrait d'os-prober"
|
|
apt_get remove os-prober
|
|
CHANGED=1
|
|
fi
|
|
[ "${PVE_KEEP_DEBIAN_KERNEL:-0}" = "1" ] && return 0
|
|
# Ne JAMAIS retirer le noyau Debian si celui de Proxmox n'est pas posé :
|
|
# la machine ne redémarrerait plus. Vérifié, pas supposé — une étape apt
|
|
# peut avoir échoué plus haut sans arrêter le reste.
|
|
if ! dpkg -s proxmox-default-kernel >/dev/null 2>&1; then
|
|
say " noyau Proxmox absent : le noyau Debian reste en place."
|
|
return 0
|
|
fi
|
|
local metas
|
|
# Tout « linux-image-* » SAUF ceux de Proxmox. L'amont écrit
|
|
# « linux-image-6.12* », la version de trixie ; le motif large couvre les
|
|
# versions suivantes, et l'exclusion protège le noyau qu'on vient de
|
|
# poser — le retirer laisserait une VM qui n'amorce plus.
|
|
# « db:Status-Status » filtre les paquets RÉELLEMENT installés : dpkg
|
|
# connaît aussi ceux qu'il a désinstallés (« config-files »,
|
|
# « not-installed »), et les passer à apt donnait « is not installed, so
|
|
# not removed » — puis un CHANGED=1 qui réclamait un redémarrage inutile.
|
|
metas="$(dpkg-query -W -f '${Package} ${db:Status-Status}\n' \
|
|
'linux-image-*' 2>/dev/null \
|
|
| awk '$2 == "installed" {print $1}' \
|
|
| grep -vE 'pve|proxmox' || true)"
|
|
if [ -n "${metas}" ]; then
|
|
say " retrait du noyau Debian : $(echo "${metas}" | tr '\n' ' ')"
|
|
# shellcheck disable=SC2086
|
|
apt_get remove ${metas}
|
|
run sudo update-grub 2>/dev/null || true
|
|
CHANGED=1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# --- 6bis. Amorçage EFI de secours -----------------------------------------
|
|
# Une image cloud amorce par le CHEMIN DE SECOURS de l'UEFI —
|
|
# \EFI\BOOT\BOOTX64.EFI — parce qu'aucune entrée NVRAM ne la nomme. Les
|
|
# paquets grub de Proxmox y recopient bien leurs binaires, mais PAS le petit
|
|
# grub.cfg qui indique où trouver la vraie configuration. GRUB s'arrête alors
|
|
# sur son invite de secours « grub> » : ni menu, ni noyau.
|
|
#
|
|
# Vécu, capture d'écran à l'appui : après le premier redémarrage, la VM brûlait
|
|
# 100 % d'un cœur SANS lire une seule fois le disque, et l'écran affichait
|
|
# « starting Boot0001 UEFI Misc Device » suivi de « grub> ».
|
|
#
|
|
# On recopie le stub que Debian a généré pour son propre chemin : il porte
|
|
# l'UUID de la racine, donc il fonctionne depuis n'importe quel chargeur.
|
|
# « grub-install --removable » ferait la même chose en réécrivant le chargeur ;
|
|
# copier un fichier est plus sûr — cela ne touche pas au chemin qui marche.
|
|
fix_efi_fallback() {
|
|
local esp="${PVE_ESP:-/boot/efi}"
|
|
local secours="${esp}/EFI/BOOT"
|
|
# sudo sur CHAQUE lecture. /boot/efi est une vfat montée « umask=077 » :
|
|
# root seul y entre, et un « [ -d ] » non privilégié y répond FAUX. Ce
|
|
# correctif ne faisait donc RIEN, en silence, et la VM retombait sur
|
|
# « grub> » au redémarrage suivant — vécu deux fois. Le glob du shell est
|
|
# aveugle pour la même raison : il faut énumérer avec sudo.
|
|
sudo test -d "${secours}" || return 0
|
|
sudo test -e "${secours}/grub.cfg" && return 0
|
|
local stub=""
|
|
# « || true » : quand aucun stub n'existe, grep ne trouve rien et rend 1
|
|
# — avec « set -o pipefail », l'affectation échoue et le script s'arrête
|
|
# AVANT d'avoir dit ce qui manque. Attrapé par un test, pas sur la machine
|
|
# réelle, où un stub existait et masquait le cas.
|
|
stub="$(sudo sh -c "ls ${esp}/EFI/*/grub.cfg 2>/dev/null" \
|
|
| grep -v '/EFI/BOOT/grub.cfg' | head -1 || true)"
|
|
if [ -z "${stub}" ]; then
|
|
say "${Yellow}⚠${Color_Off} aucun grub.cfg à recopier sous ${esp} :" \
|
|
"vérifier l'amorçage avant de redémarrer."
|
|
return 0
|
|
fi
|
|
say " amorçage de secours : $(dirname "${stub}" | xargs basename)/grub.cfg" \
|
|
"-> EFI/BOOT/"
|
|
run sudo cp "${stub}" "${secours}/grub.cfg"
|
|
CHANGED=1
|
|
}
|
|
|
|
# --- 7. Déroulé -------------------------------------------------------------
|
|
say "${Green}==>${Color_Off} Proxmox VE ${SUITE} sur ${ARCH}"
|
|
say "\n---- nom d'hôte et /etc/hosts ----"
|
|
fix_hosts
|
|
say "\n---- dépôt Proxmox ----"
|
|
add_repo
|
|
install_pve
|
|
say "\n---- ménage ----"
|
|
cleanup
|
|
say "\n---- amorçage ----"
|
|
fix_efi_fallback
|
|
|
|
IP="$(host_ip || echo localhost)"
|
|
if [ "${DRY}" = "1" ]; then
|
|
# Ne rien annoncer qui n'ait eu lieu : en dry-run, rien n'a été installé.
|
|
say "\n${Yellow}✓${Color_Off} dry-run terminé : rien n'a été changé."
|
|
say " Ce qui serait joignable ensuite : https://${IP}:8006"
|
|
exit 0
|
|
fi
|
|
say "\n${Green}✓${Color_Off} Proxmox VE installé."
|
|
say " Interface web : ${Green}https://${IP}:8006${Color_Off}"
|
|
# Proxmox authentifie par PAM : c'est le root du système qui ouvre l'interface.
|
|
# Sur une image cloud il est VERROUILLÉ (« passwd -S root » rend « L »), donc
|
|
# l'interface est inutilisable tant qu'on ne lui a pas donné un mot de passe.
|
|
# Le dire ici plutôt que de laisser chercher devant un formulaire qui refuse.
|
|
if [ "$(sudo passwd -S root 2>/dev/null | awk '{print $2}')" = "L" ]; then
|
|
say " Compte : root, ${Yellow}sans mot de passe${Color_Off} —" \
|
|
"l'interface le refusera. À faire : ${Green}sudo passwd root${Color_Off}"
|
|
else
|
|
say " Compte : root (mot de passe du système)"
|
|
fi
|
|
say " Une VM Proxmox est un hyperviseur DANS une VM : ses propres invités"
|
|
say " demandent la virtualisation imbriquée à tous les étages."
|
|
|
|
if [ "${CHANGED}" = "0" ]; then
|
|
say "\n Rien n'a changé : pas de redémarrage."
|
|
exit 0
|
|
fi
|
|
if [ "${PVE_REBOOT:-0}" != "1" ]; then
|
|
say "\n${Yellow}⚠${Color_Off} Redémarrage NÉCESSAIRE pour amorcer le" \
|
|
"noyau Proxmox : sudo reboot"
|
|
exit 0
|
|
fi
|
|
say "\n---- redémarrage pour amorcer le noyau Proxmox ----"
|
|
run sudo systemctl reboot
|