erplibre/script/proxmox/install_proxmox.sh
Mathieu Benoit adf0f275d4 [FIX] proxmox : apt-daily tient le verrou au démarrage
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)
2026-08-29 01:53:03 -04:00

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