[ADD] qemu : déployer Proxmox VE (amd64, arm64)
Proxmox ne publie aucune image cloud : son ISO est un installateur qui formate le disque. On prend donc la voie que l'amont documente lui-même — Proxmox VE sur Debian — depuis l'image cloud trixie, partagée avec un déploiement Debian 13 au lieu d'être téléchargée deux fois. s390x n'y est pas et n'y sera pas par cette voie : le dépôt n'a aucun index binary-s390x. arm64 y est, officiel depuis PVE 9. Le catalogue le dit AVANT le déploiement, au lieu d'échouer au premier apt. Vérifié sur une VM réelle : pve-manager 9.2.11, noyau 7.0.14-12-pve, quatre services actifs, interface web en HTTP 200. --- EN --- Proxmox publishes no cloud image: its ISO is an installer that formats the disk. So we take the path upstream documents itself — Proxmox VE on Debian — from the trixie cloud image, shared with a Debian 13 deployment instead of being downloaded twice. s390x is not there and will not be by this route: the repository has no binary-s390x index. arm64 is, official since PVE 9. The catalog says so BEFORE the deployment rather than failing at the first apt. Verified on a real VM: pve-manager 9.2.11, kernel 7.0.14-12-pve, four services active, web UI answering HTTP 200. Assisted-by: Claude Opus 5
This commit is contained in:
parent
770de6b0ca
commit
f12e79c3a9
8 changed files with 1073 additions and 3 deletions
495
script/proxmox/install_proxmox.sh
Executable file
495
script/proxmox/install_proxmox.sh
Executable file
|
|
@ -0,0 +1,495 @@
|
|||
#!/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
|
||||
}
|
||||
|
||||
fix_hosts() {
|
||||
local ip fqdn short
|
||||
ip="$(host_ip)" || die "aucune adresse IPv4 routable : réseau absent ?"
|
||||
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})"
|
||||
}
|
||||
|
||||
# --- 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
|
||||
}
|
||||
|
||||
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 ----"
|
||||
apt_get update
|
||||
# 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"
|
||||
[ -d "${secours}" ] || return 0
|
||||
[ -e "${secours}/grub.cfg" ] && return 0
|
||||
local stub="" candidat=""
|
||||
for candidat in "${esp}"/EFI/*/grub.cfg; do
|
||||
[ -e "${candidat}" ] || continue
|
||||
case "${candidat}" in
|
||||
*/EFI/BOOT/grub.cfg) continue ;;
|
||||
esac
|
||||
stub="${candidat}"
|
||||
break
|
||||
done
|
||||
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
|
||||
|
|
@ -184,10 +184,24 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth):
|
|||
| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
| proxmox | `9` | ✔ | ✔ | — |
|
||||
|
||||
Fedora builds s390x only for the current release, and on a separate tree
|
||||
(`fedora-secondary`) — hence the single version there.
|
||||
|
||||
`proxmox` is Proxmox VE, and it deserves a word: it publishes **no cloud
|
||||
image** — its ISO is an installer that formats the disk. So the deployment
|
||||
does what upstream itself documents for every other case, *Proxmox VE on
|
||||
Debian*: it downloads the **Debian trixie** cloud image (the very same file, so
|
||||
a Debian 13 and a Proxmox deployment share one download) and the `pve`
|
||||
packages turn it into a hypervisor — Proxmox kernel, web UI on `:8006`.
|
||||
|
||||
The version number is Proxmox's, not Debian's: PVE 9 = trixie. arm64 has been
|
||||
official since PVE 9 (the upstream `trixie` Release announces `amd64 arm64`,
|
||||
and the arm64 index really serves `proxmox-ve`). s390x is absent and will stay
|
||||
so by this route: the repository has no `binary-s390x` index at all — the
|
||||
catalog says it before the deployment rather than failing at the first `apt`.
|
||||
|
||||
`opensuse` covers two distinct products, not two versions of one. Leap `16.0`
|
||||
is numbered and stable (SLE base) and is the default. `tumbleweed` is the
|
||||
rolling one, kept as a bellwether for breakage to come: its snapshot drift is
|
||||
|
|
@ -218,6 +232,7 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) :
|
|||
| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (défaut), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
| proxmox | `9` | ✔ | ✔ | — |
|
||||
|
||||
Fedora ne construit s390x que pour la version courante, et sur une
|
||||
arborescence à part (`fedora-secondary`) — d'où la version unique.
|
||||
|
|
@ -232,6 +247,38 @@ Les deux livrent un qpdf au-dessus du seuil de pikepdf : la compilation de
|
|||
qpdf, une demi-heure, ne s'y déclenche jamais — ce qui compte sous émulation
|
||||
s390x.
|
||||
|
||||
`proxmox`, c'est Proxmox VE, et il mérite un mot : il ne publie **aucune image
|
||||
cloud** — son ISO est un installateur qui formate le disque. Le déploiement
|
||||
fait donc ce que l'amont documente lui-même pour tous les autres cas,
|
||||
*Proxmox VE sur Debian* : il télécharge l'image cloud **Debian trixie** (le
|
||||
même fichier, si bien qu'un déploiement Debian 13 et un Proxmox se partagent un
|
||||
seul téléchargement) et les paquets `pve` en font un hyperviseur — noyau
|
||||
Proxmox, interface web sur `:8006`.
|
||||
|
||||
Le numéro de version est celui de Proxmox, pas de Debian : PVE 9 = trixie.
|
||||
arm64 est officiel depuis PVE 9 (le Release `trixie` de l'amont annonce
|
||||
`amd64 arm64`, et l'index arm64 sert bien `proxmox-ve`). s390x est absent et le
|
||||
restera par cette voie : le dépôt n'a aucun index `binary-s390x` — le catalogue
|
||||
le dit avant le déploiement plutôt que d'échouer au premier `apt`.
|
||||
|
||||
Une VM Proxmox est un hyperviseur DANS une VM : ses propres invités demandent
|
||||
la virtualisation imbriquée à tous les étages. L'installation se fait par le
|
||||
profil « Hyperviseur Proxmox VE (sans Odoo) » du menu de déploiement, ou à la
|
||||
main dans la VM :
|
||||
|
||||
```bash
|
||||
sudo ./script/proxmox/install_proxmox.sh --dry-run # dit ce qu'il ferait
|
||||
sudo ./script/proxmox/install_proxmox.sh # puis : sudo reboot
|
||||
```
|
||||
|
||||
Trois pièges de l'image cloud, tous rencontrés sur une VM réelle et traités par
|
||||
le script : cloud-init tient encore le verrou d'`apt` au premier démarrage ;
|
||||
`grub-pc`, tiré par les paquets `pve`, demande sur quel disque s'installer et
|
||||
bloque toute la transaction sans préréponse ; et le chemin de secours UEFI
|
||||
(`\EFI\BOOT\`) reçoit les binaires GRUB de Proxmox mais pas le `grub.cfg`
|
||||
qui dit où trouver la configuration — sans quoi la VM s'arrête sur l'invite
|
||||
`grub>`, sans menu ni noyau.
|
||||
|
||||
Fournissez un chemin d'image en argument positionnel pour surcharger
|
||||
l'emplacement de téléchargement automatique.
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ Catalogue, par architecture (`deploy_qemu.py` fait autorité) :
|
|||
| rocky | `9`, `10` (défaut) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (défaut), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
| proxmox | `9` | ✔ | ✔ | — |
|
||||
|
||||
Fedora ne construit s390x que pour la version courante, et sur une
|
||||
arborescence à part (`fedora-secondary`) — d'où la version unique.
|
||||
|
|
@ -117,6 +118,38 @@ Les deux livrent un qpdf au-dessus du seuil de pikepdf : la compilation de
|
|||
qpdf, une demi-heure, ne s'y déclenche jamais — ce qui compte sous émulation
|
||||
s390x.
|
||||
|
||||
`proxmox`, c'est Proxmox VE, et il mérite un mot : il ne publie **aucune image
|
||||
cloud** — son ISO est un installateur qui formate le disque. Le déploiement
|
||||
fait donc ce que l'amont documente lui-même pour tous les autres cas,
|
||||
*Proxmox VE sur Debian* : il télécharge l'image cloud **Debian trixie** (le
|
||||
même fichier, si bien qu'un déploiement Debian 13 et un Proxmox se partagent un
|
||||
seul téléchargement) et les paquets `pve` en font un hyperviseur — noyau
|
||||
Proxmox, interface web sur `:8006`.
|
||||
|
||||
Le numéro de version est celui de Proxmox, pas de Debian : PVE 9 = trixie.
|
||||
arm64 est officiel depuis PVE 9 (le Release `trixie` de l'amont annonce
|
||||
`amd64 arm64`, et l'index arm64 sert bien `proxmox-ve`). s390x est absent et le
|
||||
restera par cette voie : le dépôt n'a aucun index `binary-s390x` — le catalogue
|
||||
le dit avant le déploiement plutôt que d'échouer au premier `apt`.
|
||||
|
||||
Une VM Proxmox est un hyperviseur DANS une VM : ses propres invités demandent
|
||||
la virtualisation imbriquée à tous les étages. L'installation se fait par le
|
||||
profil « Hyperviseur Proxmox VE (sans Odoo) » du menu de déploiement, ou à la
|
||||
main dans la VM :
|
||||
|
||||
```bash
|
||||
sudo ./script/proxmox/install_proxmox.sh --dry-run # dit ce qu'il ferait
|
||||
sudo ./script/proxmox/install_proxmox.sh # puis : sudo reboot
|
||||
```
|
||||
|
||||
Trois pièges de l'image cloud, tous rencontrés sur une VM réelle et traités par
|
||||
le script : cloud-init tient encore le verrou d'`apt` au premier démarrage ;
|
||||
`grub-pc`, tiré par les paquets `pve`, demande sur quel disque s'installer et
|
||||
bloque toute la transaction sans préréponse ; et le chemin de secours UEFI
|
||||
(`\EFI\BOOT\`) reçoit les binaires GRUB de Proxmox mais pas le `grub.cfg`
|
||||
qui dit où trouver la configuration — sans quoi la VM s'arrête sur l'invite
|
||||
`grub>`, sans menu ni noyau.
|
||||
|
||||
Fournissez un chemin d'image en argument positionnel pour surcharger
|
||||
l'emplacement de téléchargement automatique.
|
||||
|
||||
|
|
|
|||
|
|
@ -97,10 +97,24 @@ Catalog, per architecture (`deploy_qemu.py` is the source of truth):
|
|||
| rocky | `9`, `10` (default) | ✔ | ✔ | ✔ |
|
||||
| opensuse | `16.0` (default), `tumbleweed` | ✔ | ✔ | ✔ |
|
||||
| arch | `latest` | ✔ | — | — |
|
||||
| proxmox | `9` | ✔ | ✔ | — |
|
||||
|
||||
Fedora builds s390x only for the current release, and on a separate tree
|
||||
(`fedora-secondary`) — hence the single version there.
|
||||
|
||||
`proxmox` is Proxmox VE, and it deserves a word: it publishes **no cloud
|
||||
image** — its ISO is an installer that formats the disk. So the deployment
|
||||
does what upstream itself documents for every other case, *Proxmox VE on
|
||||
Debian*: it downloads the **Debian trixie** cloud image (the very same file, so
|
||||
a Debian 13 and a Proxmox deployment share one download) and the `pve`
|
||||
packages turn it into a hypervisor — Proxmox kernel, web UI on `:8006`.
|
||||
|
||||
The version number is Proxmox's, not Debian's: PVE 9 = trixie. arm64 has been
|
||||
official since PVE 9 (the upstream `trixie` Release announces `amd64 arm64`,
|
||||
and the arm64 index really serves `proxmox-ve`). s390x is absent and will stay
|
||||
so by this route: the repository has no `binary-s390x` index at all — the
|
||||
catalog says it before the deployment rather than failing at the first `apt`.
|
||||
|
||||
`opensuse` covers two distinct products, not two versions of one. Leap `16.0`
|
||||
is numbered and stable (SLE base) and is the default. `tumbleweed` is the
|
||||
rolling one, kept as a bellwether for breakage to come: its snapshot drift is
|
||||
|
|
|
|||
|
|
@ -140,6 +140,30 @@ ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|||
"latest": ("latest", "archlinux", 1024, "20G"),
|
||||
}
|
||||
|
||||
# Proxmox VE ne publie AUCUNE image cloud : son ISO est un installateur, et la
|
||||
# voie que l'amont documente pour tout le reste est « Proxmox VE sur Debian ».
|
||||
# La base est donc l'image cloud Debian, que les paquets pve transforment en
|
||||
# hyperviseur. Le numéro est celui de PROXMOX, pas de Debian : PVE 9 = trixie.
|
||||
#
|
||||
# arm64 est officiel depuis PVE 9 — vérifié dans le dépôt amont, dont le
|
||||
# Release de trixie annonce « amd64 arm64 » et dont l'index arm64 sert bien
|
||||
# proxmox-ve. bookworm (PVE 8), lui, est amd64 seulement : d'où une seule
|
||||
# version au catalogue, celle qui couvre les deux architectures.
|
||||
#
|
||||
# s390x n'y figure pas et n'y figurera pas par cette voie : l'index
|
||||
# « binary-s390x » du dépôt répond 404. Ce n'est pas une difficulté, c'est une
|
||||
# absence — et ARCH_DISTRO_SUPPORT la dit à la place d'un échec au montage.
|
||||
#
|
||||
# RAM/disque : 4 Gio et 32 Go, pas les 1 Gio/20 Go de Debian. Proxmox demande
|
||||
# 2 Gio pour lui seul, et l'installation télécharge son propre noyau ; 20 Go ne
|
||||
# laisseraient pas la place d'une seule VM invitée.
|
||||
PROXMOX_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
||||
"9": ("trixie", "debian13", 4096, "32G"),
|
||||
}
|
||||
|
||||
# Version Debian dont l'image sert de base à chaque version de Proxmox.
|
||||
PROXMOX_DEBIAN_BASE: dict[str, str] = {"9": "13"}
|
||||
|
||||
# distro -> (table des versions, version par défaut).
|
||||
DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
|
||||
"ubuntu": (UBUNTU_VERSIONS, "24.04"),
|
||||
|
|
@ -149,6 +173,7 @@ DISTROS: dict[str, tuple[dict[str, tuple[str, str, int, str]], str]] = {
|
|||
"rocky": (ROCKY_VERSIONS, "10"),
|
||||
"opensuse": (OPENSUSE_VERSIONS, "16.0"),
|
||||
"arch": (ARCH_VERSIONS, "latest"),
|
||||
"proxmox": (PROXMOX_VERSIONS, "9"),
|
||||
}
|
||||
|
||||
# Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro.
|
||||
|
|
@ -235,6 +260,7 @@ ARM64_DISTROS: tuple[str, ...] = (
|
|||
"almalinux",
|
||||
"rocky",
|
||||
"opensuse",
|
||||
"proxmox",
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -429,6 +455,13 @@ def image_candidates(
|
|||
f"{base}/{code}/latest/debian-{version}-genericcloud-{a}.qcow2"
|
||||
for base in DEBIAN_CLOUD_BASES
|
||||
]
|
||||
if distro == "proxmox":
|
||||
# C'est bien l'image DEBIAN qu'on télécharge : « proxmox-ve », posé
|
||||
# dessus, en fait l'hyperviseur. Le miroir ne connaît que le numéro de
|
||||
# Debian, jamais celui de Proxmox.
|
||||
return image_candidates(
|
||||
"debian", code, arch, PROXMOX_DEBIAN_BASE[version], dry_run
|
||||
)
|
||||
if distro == "fedora":
|
||||
return [resolve_fedora_url(version, arch, dry_run)]
|
||||
if distro == "almalinux":
|
||||
|
|
@ -525,6 +558,14 @@ def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
|
|||
return f"ubuntu-{version}-server-cloudimg-{a}.img"
|
||||
if distro == "debian":
|
||||
return f"debian-{version}-genericcloud-{a}.qcow2"
|
||||
if distro == "proxmox":
|
||||
# Le fichier téléchargé EST celui de Debian : lui donner le même nom
|
||||
# de cache fait qu'un déploiement Debian 13 et un Proxmox se
|
||||
# PARTAGENT le téléchargement (325 Mio) au lieu d'en faire deux. Sans
|
||||
# cette branche, le repli de fin nommait l'image « fedora-cloud-9 ».
|
||||
return (
|
||||
f"debian-{PROXMOX_DEBIAN_BASE[version]}-genericcloud-{a}.qcow2"
|
||||
)
|
||||
if distro == "arch":
|
||||
return f"arch-linux-{a}-cloudimg.qcow2"
|
||||
if distro == "opensuse":
|
||||
|
|
@ -1615,6 +1656,7 @@ DISTRO_LABELS: dict[str, str] = {
|
|||
"rocky": "Rocky Linux",
|
||||
"opensuse": "openSUSE",
|
||||
"arch": "Arch Linux",
|
||||
"proxmox": "Proxmox VE",
|
||||
}
|
||||
|
||||
# Gestionnaire de paquets de chaque distribution du catalogue.
|
||||
|
|
@ -1626,6 +1668,8 @@ DISTRO_PKG: dict[str, str] = {
|
|||
"rocky": "dnf",
|
||||
"opensuse": "zypper",
|
||||
"arch": "pacman",
|
||||
# Debian dessous : c'est apt qui sert, et le guide de connexion le dit.
|
||||
"proxmox": "apt",
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1052,6 +1052,10 @@ class TODO:
|
|||
# deploy_qemu.py, qui fait autorité sur le catalogue.
|
||||
"opensuse": (["16.0", "tumbleweed"], "16.0"),
|
||||
"arch": (["latest"], "latest"),
|
||||
# Proxmox VE : le numéro est celui de PVE, pas de Debian (9 = trixie).
|
||||
# Une seule version au catalogue, la seule qui couvre amd64 ET arm64 —
|
||||
# voir PROXMOX_VERSIONS dans deploy_qemu.py, qui fait autorité.
|
||||
"proxmox": (["9"], "9"),
|
||||
}
|
||||
|
||||
def _qemu_prompt_distro(self):
|
||||
|
|
@ -5339,13 +5343,25 @@ class TODO:
|
|||
"make install_os && make install_dev && "
|
||||
+ self._QEMU_QEMU_PKGS,
|
||||
),
|
||||
(
|
||||
t("Proxmox VE hypervisor (no Odoo)"),
|
||||
"./script/proxmox/install_proxmox.sh",
|
||||
),
|
||||
]
|
||||
return profiles
|
||||
|
||||
def _qemu_pick_install_profile(self):
|
||||
def _qemu_pick_install_profile(self, distro=""):
|
||||
"""Choix de CE QU'ON installe sur la VM. Renvoie (label, commande
|
||||
finale exécutée dans ~/git/erplibre)."""
|
||||
finale exécutée dans ~/git/erplibre).
|
||||
|
||||
Sur une VM Proxmox, le profil hyperviseur passe en tête : choisir
|
||||
« Proxmox VE » comme système, c'est demander qu'il soit installé, et
|
||||
laisser Odoo 18 en défaut ferait poser un ERP sur un hyperviseur.
|
||||
"""
|
||||
profiles = self._qemu_install_profiles()
|
||||
if distro == "proxmox":
|
||||
pve = t("Proxmox VE hypervisor (no Odoo)")
|
||||
profiles.sort(key=lambda p: p[0] != pve)
|
||||
print(f"\n{t('What to install on the VM(s)?')}")
|
||||
for i, (label, _cmd) in enumerate(profiles, 1):
|
||||
print(f" [{i}] {label}{' *' if i == 1 else ''}")
|
||||
|
|
@ -8922,11 +8938,28 @@ class TODO:
|
|||
ans = input(
|
||||
t("Install ERPLibre into ~/git/erplibre on each VM? (Y/n): ")
|
||||
)
|
||||
if not self._is_yes_default_yes(ans) and any(
|
||||
v.get("distro") == "proxmox" for v in vms
|
||||
):
|
||||
# C'est par cette étape que passe l'installation de Proxmox : sans
|
||||
# elle la VM reste une Debian nue, ce qui n'est pas ce qu'on a
|
||||
# demandé en choisissant « Proxmox VE » comme système.
|
||||
print(
|
||||
f"\n ⚠ {t('Proxmox will NOT be installed: plain Debian VM.')}"
|
||||
)
|
||||
print(
|
||||
f" {t('Later, in the VM:')}"
|
||||
" sudo ./script/proxmox/install_proxmox.sh"
|
||||
)
|
||||
if self._is_yes_default_yes(ans):
|
||||
branch = self._qemu_pick_branch()
|
||||
# dev (~/git, SELinux relâché) vs prod (/opt, confiné)
|
||||
prod = self._qemu_ask_prod()
|
||||
label, cmd = self._qemu_pick_install_profile()
|
||||
label, cmd = self._qemu_pick_install_profile(
|
||||
"proxmox"
|
||||
if any(v.get("distro") == "proxmox" for v in vms)
|
||||
else ""
|
||||
)
|
||||
monitor = self._is_yes_default_yes(
|
||||
input(t("Interactive monitoring dashboard? (y/N): "))
|
||||
)
|
||||
|
|
|
|||
|
|
@ -3115,6 +3115,18 @@ TRANSLATIONS = {
|
|||
"fr": "Nœud de rendu",
|
||||
"en": "Render node",
|
||||
},
|
||||
"Proxmox VE hypervisor (no Odoo)": {
|
||||
"fr": "Hyperviseur Proxmox VE (sans Odoo)",
|
||||
"en": "Proxmox VE hypervisor (no Odoo)",
|
||||
},
|
||||
"Proxmox will NOT be installed: plain Debian VM.": {
|
||||
"fr": "Proxmox ne sera PAS installé : la VM restera une Debian nue.",
|
||||
"en": "Proxmox will NOT be installed: plain Debian VM.",
|
||||
},
|
||||
"Later, in the VM:": {
|
||||
"fr": "Plus tard, dans la VM :",
|
||||
"en": "Later, in the VM:",
|
||||
},
|
||||
"sshfs mount failed.": {
|
||||
"fr": "Le montage sshfs a échoué.",
|
||||
"en": "sshfs mount failed.",
|
||||
|
|
|
|||
392
test/test_qemu_proxmox.py
Normal file
392
test/test_qemu_proxmox.py
Normal file
|
|
@ -0,0 +1,392 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""Proxmox VE au catalogue de déploiement : ce qui existe, et ce qui n'existe pas.
|
||||
|
||||
Proxmox ne publie AUCUNE image cloud — son ISO est un installateur qui formate
|
||||
le disque. La voie que l'amont documente pour tout le reste est « Proxmox VE
|
||||
sur Debian » : on part de l'image cloud Debian trixie et les paquets pve en
|
||||
font l'hyperviseur. Le catalogue le dit ainsi, et l'image téléchargée est
|
||||
littéralement celle de Debian.
|
||||
|
||||
Par architecture, vérifié dans le dépôt amont et non déduit :
|
||||
|
||||
- amd64 : 1800 paquets, proxmox-ve présent.
|
||||
- arm64 : 448 paquets, proxmox-ve présent — officiel depuis PVE 9, dont le
|
||||
Release de trixie annonce « amd64 arm64 ».
|
||||
- s390x : l'index « binary-s390x » répond 404. Ce n'est pas une difficulté,
|
||||
c'est une absence, et le catalogue la dit avant le déploiement.
|
||||
|
||||
Ce que ces tests gardent :
|
||||
|
||||
- Le nom de cache est celui de Debian : sans lui, le repli de fin nommait
|
||||
l'image « fedora-cloud-9 » et un même téléchargement se faisait deux fois.
|
||||
- Les planchers RAM/disque sont ceux d'un hyperviseur, pas ceux de Debian.
|
||||
- Le retrait du noyau Debian n'emporte JAMAIS celui de Proxmox — sans quoi la
|
||||
VM ne redémarrerait plus.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
RACINE = pathlib.Path(__file__).resolve().parents[1]
|
||||
SCRIPT = RACINE / "script/proxmox/install_proxmox.sh"
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO # noqa: E402
|
||||
from script.todo.todo_i18n import t # noqa: E402
|
||||
|
||||
|
||||
def _deploy():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"deploy_qemu", RACINE / "script/qemu/deploy_qemu.py"
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
DQ = _deploy()
|
||||
|
||||
# Somme publiée par l'amont sur « Install Proxmox VE on Debian 13 Trixie », et
|
||||
# recopiée ici EXPRÈS : deux copies indépendantes, c'est ce qui donne son sens
|
||||
# à un condensat épinglé. Si l'une change sans l'autre, ce test le dit.
|
||||
KEY_SHA256 = "136673be77aba35dcce385b28737689ad64fd785a797e57897589aed08db6e45"
|
||||
|
||||
|
||||
class TestCatalogue(unittest.TestCase):
|
||||
def test_it_is_offered_as_a_distro(self):
|
||||
self.assertIn("proxmox", DQ.DISTROS)
|
||||
versions, defaut = DQ.DISTROS["proxmox"]
|
||||
self.assertEqual("9", defaut)
|
||||
self.assertIn("9", versions)
|
||||
|
||||
def test_the_base_is_the_debian_trixie_cloud_image(self):
|
||||
"""C'est bien l'image DEBIAN qu'on télécharge : Proxmox n'en publie
|
||||
aucune."""
|
||||
for arch in ("amd64", "arm64"):
|
||||
url = DQ.image_url("proxmox", "trixie", arch, "9")
|
||||
self.assertIn("trixie", url)
|
||||
self.assertIn(f"debian-13-genericcloud-{arch}.qcow2", url)
|
||||
|
||||
def test_the_cache_name_is_debians_so_the_download_is_shared(self):
|
||||
"""Un déploiement Debian 13 et un Proxmox visent le MÊME fichier : le
|
||||
repli de fin nommait l'image « fedora-cloud-9 », et 325 Mio se
|
||||
retéléchargeaient."""
|
||||
pve = DQ.default_image_name("proxmox", "trixie", "amd64", "9")
|
||||
deb = DQ.default_image_name("debian", "trixie", "amd64", "13")
|
||||
self.assertEqual(deb, pve)
|
||||
self.assertNotIn("fedora", pve)
|
||||
|
||||
def test_s390x_does_not_exist_and_is_refused(self):
|
||||
"""Le dépôt amont n'a pas d'index s390x (404). Le refus vaut mieux
|
||||
qu'un déploiement qui échouera au premier apt."""
|
||||
self.assertNotIn("proxmox", DQ.ARCH_DISTRO_SUPPORT["s390x"])
|
||||
|
||||
def test_arm64_is_official_since_pve_9(self):
|
||||
self.assertIn("proxmox", DQ.ARCH_DISTRO_SUPPORT["arm64"])
|
||||
|
||||
def test_the_floors_are_a_hypervisors_not_debians(self):
|
||||
"""Proxmox demande 2 Gio pour lui seul et télécharge son noyau : les
|
||||
1 Gio/20 Go de Debian donneraient une VM incapable d'héberger une
|
||||
seule invitée."""
|
||||
_code, _osinfo, ram, disque = DQ.PROXMOX_VERSIONS["9"]
|
||||
self.assertGreaterEqual(ram, 4096)
|
||||
self.assertGreaterEqual(int(disque.rstrip("G")), 32)
|
||||
deb_ram = DQ.DEBIAN_VERSIONS["13"][2]
|
||||
self.assertGreater(ram, deb_ram)
|
||||
|
||||
def test_it_reuses_debians_osinfo(self):
|
||||
"""Le système EST une Debian : libosinfo n'a pas d'entrée Proxmox, et
|
||||
en inventer une ferait échouer virt-install."""
|
||||
self.assertEqual("debian13", DQ.PROXMOX_VERSIONS["9"][1])
|
||||
|
||||
def test_it_is_named_after_proxmox_not_after_the_key(self):
|
||||
self.assertEqual("Proxmox VE 9", DQ.distro_label("proxmox", "9"))
|
||||
|
||||
def test_the_login_guide_knows_it_is_apt(self):
|
||||
self.assertEqual("apt", DQ.DISTRO_PKG["proxmox"])
|
||||
|
||||
|
||||
class TestLesDeuxCatalogues(unittest.TestCase):
|
||||
"""todo.py duplique le catalogue de deploy_qemu.py. Qu'ils s'accordent."""
|
||||
|
||||
def test_the_menu_offers_every_distro_of_the_catalogue(self):
|
||||
menu = set(TODO._QEMU_DISTROS)
|
||||
catalogue = set(DQ.DISTROS)
|
||||
self.assertEqual(
|
||||
catalogue,
|
||||
menu,
|
||||
"les deux catalogues divergent : "
|
||||
f"menu seul {menu - catalogue}, deploy seul {catalogue - menu}",
|
||||
)
|
||||
|
||||
def test_the_versions_agree_for_proxmox(self):
|
||||
versions, defaut = TODO._QEMU_DISTROS["proxmox"]
|
||||
self.assertEqual(["9"], list(versions))
|
||||
self.assertEqual("9", defaut)
|
||||
|
||||
|
||||
class TestLeProfil(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
|
||||
def test_a_dedicated_profile_calls_the_dedicated_script(self):
|
||||
profils = dict(self.todo._qemu_install_profiles())
|
||||
libelle = t("Proxmox VE hypervisor (no Odoo)")
|
||||
self.assertIn(libelle, profils)
|
||||
self.assertIn("install_proxmox.sh", profils[libelle])
|
||||
|
||||
def test_on_a_proxmox_vm_it_comes_first(self):
|
||||
"""Laisser « Odoo 18 » en défaut ferait poser un ERP sur un
|
||||
hyperviseur : c'est le contraire de ce qu'on a demandé."""
|
||||
vus = []
|
||||
with mock.patch("builtins.input", lambda *a: ""):
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
libelle, cmd = self.todo._qemu_pick_install_profile("proxmox")
|
||||
vus.append((libelle, cmd))
|
||||
self.assertIn("install_proxmox.sh", cmd)
|
||||
|
||||
def test_on_any_other_vm_odoo_stays_the_default(self):
|
||||
import contextlib
|
||||
import io
|
||||
|
||||
with mock.patch("builtins.input", lambda *a: ""):
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
libelle, cmd = self.todo._qemu_pick_install_profile("debian")
|
||||
self.assertNotIn("install_proxmox.sh", cmd)
|
||||
self.assertIn("install_odoo_18", cmd)
|
||||
|
||||
|
||||
class TestLeScript(unittest.TestCase):
|
||||
def test_it_is_executable_and_valid_shell(self):
|
||||
self.assertTrue(os.access(SCRIPT, os.X_OK), "pas exécutable")
|
||||
res = subprocess.run(
|
||||
["bash", "-n", str(SCRIPT)], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
|
||||
def test_help_names_the_knobs_and_exits_clean(self):
|
||||
res = subprocess.run(
|
||||
["bash", str(SCRIPT), "--help"], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(0, res.returncode)
|
||||
for knob in ("--dry-run", "PVE_SUITE", "PVE_REBOOT"):
|
||||
self.assertIn(knob, res.stdout)
|
||||
|
||||
def test_the_pinned_key_hash_is_the_published_one(self):
|
||||
texte = SCRIPT.read_text(encoding="utf-8")
|
||||
self.assertIn(KEY_SHA256, texte)
|
||||
self.assertIn("proxmox-archive-keyring-", texte)
|
||||
|
||||
def _lance(self, args=(), stubs=None, env=None):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bin_dir = pathlib.Path(tmp) / "bin"
|
||||
bin_dir.mkdir()
|
||||
for nom, corps in (stubs or {}).items():
|
||||
(bin_dir / nom).write_text(f"#!/bin/bash\n{corps}\n")
|
||||
(bin_dir / nom).chmod(0o755)
|
||||
osrel = pathlib.Path(tmp) / "os-release"
|
||||
osrel.write_text("ID=debian\nVERSION_CODENAME=trixie\n")
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT), *args],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(
|
||||
os.environ,
|
||||
PATH=f"{bin_dir}:/usr/bin:/bin",
|
||||
PVE_OS_RELEASE=str(osrel),
|
||||
**(env or {}),
|
||||
),
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
def test_an_unpublished_architecture_is_refused_by_name(self):
|
||||
res = self._lance(stubs={"uname": "echo s390x"})
|
||||
self.assertNotEqual(0, res.returncode)
|
||||
self.assertIn("s390x", res.stdout + res.stderr)
|
||||
|
||||
def test_another_distribution_is_refused_by_name(self):
|
||||
"""Le script s'installe SUR Debian : le dire vaut mieux qu'un apt qui
|
||||
échouera sur un dépôt introuvable."""
|
||||
with tempfile.NamedTemporaryFile(
|
||||
"w", suffix=".os", delete=False
|
||||
) as fh:
|
||||
fh.write("ID=ubuntu\nVERSION_CODENAME=noble\n")
|
||||
chemin = fh.name
|
||||
self.addCleanup(os.unlink, chemin)
|
||||
res = subprocess.run(
|
||||
["bash", str(SCRIPT)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(os.environ, PVE_OS_RELEASE=chemin),
|
||||
timeout=60,
|
||||
)
|
||||
self.assertNotEqual(0, res.returncode)
|
||||
self.assertIn("ubuntu", res.stdout + res.stderr)
|
||||
|
||||
def test_the_dry_run_follows_the_upstream_procedure(self):
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertEqual(0, res.returncode, res.stdout + res.stderr)
|
||||
out = res.stdout
|
||||
for attendu in (
|
||||
"proxmox-archive-keyring-trixie.gpg",
|
||||
"Types: deb",
|
||||
"pve-no-subscription",
|
||||
"Signed-By: /usr/share/keyrings/proxmox-archive-keyring.gpg",
|
||||
"proxmox-default-kernel",
|
||||
"proxmox-ve postfix open-iscsi chrony",
|
||||
):
|
||||
self.assertIn(attendu, out, attendu)
|
||||
|
||||
def test_the_dry_run_changes_nothing_and_says_so(self):
|
||||
"""La leçon du montage sshfs : ne rien annoncer qui n'ait eu lieu."""
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertIn("dry-run", res.stdout)
|
||||
self.assertNotIn("Proxmox VE installé", res.stdout)
|
||||
|
||||
def test_postfix_is_preseeded_or_the_install_hangs(self):
|
||||
"""postfix pose deux questions debconf : sans préréponse, une
|
||||
installation lancée par SSH reste pendue sur une saisie invisible."""
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertIn("debconf-set-selections", res.stdout)
|
||||
self.assertIn("main_mailer_type", res.stdout)
|
||||
|
||||
def test_it_never_removes_the_proxmox_kernel(self):
|
||||
"""Le noyau qu'on vient de poser : l'emporter laisserait une VM qui
|
||||
n'amorce plus. dpkg-query rend ici les deux, seul celui de Debian doit
|
||||
partir."""
|
||||
res = self._lance(
|
||||
["--dry-run"],
|
||||
stubs={
|
||||
"uname": "echo x86_64",
|
||||
"dpkg": "exit 0",
|
||||
# Format réel de la requête : nom + état. dpkg connaît
|
||||
# aussi les paquets DÉSINSTALLÉS (« config-files »), et les
|
||||
# passer à apt réclamait un redémarrage pour rien.
|
||||
"dpkg-query": (
|
||||
"printf 'linux-image-6.12.0-amd64 installed\\n"
|
||||
"linux-image-amd64 config-files\\n"
|
||||
"linux-image-6.14.11-1-pve installed\\n'"
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stdout + res.stderr)
|
||||
ligne = [
|
||||
x for x in res.stdout.splitlines() if "remove linux-image" in x
|
||||
]
|
||||
self.assertTrue(ligne, res.stdout)
|
||||
self.assertIn("linux-image-6.12.0-amd64", ligne[0])
|
||||
self.assertNotIn("pve", ligne[0])
|
||||
# « config-files » = déjà désinstallé : ne pas le repasser à apt.
|
||||
self.assertNotIn("linux-image-amd64", ligne[0])
|
||||
|
||||
def test_without_the_proxmox_kernel_nothing_is_removed(self):
|
||||
"""Une étape apt a pu échouer plus haut : on ne touche pas au noyau
|
||||
Debian tant que celui de Proxmox n'est pas là."""
|
||||
res = self._lance(
|
||||
["--dry-run"],
|
||||
stubs={"uname": "echo x86_64", "dpkg": "exit 1"},
|
||||
)
|
||||
self.assertNotIn("remove linux-image", res.stdout)
|
||||
self.assertIn("noyau Debian reste en place", res.stdout)
|
||||
|
||||
def test_grub_pc_is_preseeded_or_dpkg_stops(self):
|
||||
"""Le piège propre à l'image cloud : elle amorce en EFI, les paquets
|
||||
pve tirent grub-pc, et sa post-installation refuse de deviner le
|
||||
disque — « You must correct your GRUB install devices before
|
||||
proceeding ». Mesuré : dpkg s'arrête et emporte la transaction."""
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertIn("grub-pc/install_devices", res.stdout)
|
||||
self.assertRegex(res.stdout, r"install_devices multiselect /dev/\w+")
|
||||
|
||||
def test_apt_waits_for_the_lock_instead_of_giving_up(self):
|
||||
"""Sur une VM fraîche, cloud-init tient encore le verrou : mesuré,
|
||||
« held by process 996 (apt-get) », et le script mourait 40 secondes
|
||||
après le démarrage."""
|
||||
texte = SCRIPT.read_text(encoding="utf-8")
|
||||
self.assertIn("DPkg::Lock::Timeout", texte)
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertIn("DPkg::Lock::Timeout", res.stdout)
|
||||
|
||||
def test_it_waits_for_cloud_init_but_not_for_its_verdict(self):
|
||||
"""cloud-init rend « error » sur l'image Debian 13 pour deux modules
|
||||
sans rapport (console-setup absent, update-locale) alors qu'il a bien
|
||||
fini. On attend qu'il termine, pas qu'il soit content."""
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertIn("cloud-init status --wait", res.stdout)
|
||||
# L'échec de cloud-init ne doit PAS arrêter le script : « || true »
|
||||
# sur l'appel réel, pas sur l'écho du dry-run.
|
||||
texte = SCRIPT.read_text(encoding="utf-8")
|
||||
reel = [
|
||||
x
|
||||
for x in texte.splitlines()
|
||||
if "cloud-init status --wait" in x and "dry-run" not in x
|
||||
]
|
||||
self.assertTrue(reel, texte)
|
||||
self.assertIn("|| true", reel[0])
|
||||
|
||||
def _esp(self, avec_secours):
|
||||
"""Fausse partition EFI : un stub Debian, et le chemin de secours."""
|
||||
racine = tempfile.mkdtemp()
|
||||
self.addCleanup(
|
||||
lambda: subprocess.run(["rm", "-rf", racine], check=False)
|
||||
)
|
||||
deb = pathlib.Path(racine, "EFI", "debian")
|
||||
boot = pathlib.Path(racine, "EFI", "BOOT")
|
||||
deb.mkdir(parents=True)
|
||||
boot.mkdir(parents=True)
|
||||
(deb / "grub.cfg").write_text(
|
||||
"search.fs_uuid 81e2a465 root\nset prefix=($root)'/boot/grub'\n"
|
||||
)
|
||||
(boot / "BOOTX64.EFI").write_text("binaire")
|
||||
if avec_secours:
|
||||
(boot / "grub.cfg").write_text("déjà là")
|
||||
return racine
|
||||
|
||||
def test_the_efi_fallback_stub_is_restored(self):
|
||||
"""Sans ce fichier, GRUB s'arrête sur « grub> » : ni menu ni noyau.
|
||||
Vécu après le premier redémarrage — la VM brûlait 100 % d'un cœur sans
|
||||
lire une seule fois le disque, capture d'écran à l'appui."""
|
||||
esp = self._esp(avec_secours=False)
|
||||
res = self._lance(["--dry-run"], env={"PVE_ESP": esp})
|
||||
self.assertEqual(0, res.returncode, res.stdout + res.stderr)
|
||||
self.assertIn("amorçage de secours", res.stdout)
|
||||
self.assertIn("EFI/BOOT/", res.stdout)
|
||||
|
||||
def test_a_working_fallback_is_left_alone(self):
|
||||
"""Ne pas réécrire ce qui marche : le stub en place peut avoir été
|
||||
ajusté à la main."""
|
||||
esp = self._esp(avec_secours=True)
|
||||
res = self._lance(["--dry-run"], env={"PVE_ESP": esp})
|
||||
self.assertNotIn("amorçage de secours", res.stdout)
|
||||
|
||||
def test_an_esp_without_any_stub_is_said_not_guessed(self):
|
||||
racine = tempfile.mkdtemp()
|
||||
self.addCleanup(
|
||||
lambda: subprocess.run(["rm", "-rf", racine], check=False)
|
||||
)
|
||||
pathlib.Path(racine, "EFI", "BOOT").mkdir(parents=True)
|
||||
res = self._lance(["--dry-run"], env={"PVE_ESP": racine})
|
||||
self.assertIn("aucun grub.cfg à recopier", res.stdout)
|
||||
|
||||
def test_it_does_not_reboot_unless_asked(self):
|
||||
"""Lancé par SSH depuis le déploiement, un reboot couperait la session
|
||||
et ferait passer une installation réussie pour un échec."""
|
||||
texte = SCRIPT.read_text(encoding="utf-8")
|
||||
self.assertIn('PVE_REBOOT:-0}" != "1"', texte)
|
||||
res = self._lance(["--dry-run"])
|
||||
self.assertNotIn("systemctl reboot", res.stdout)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=1)
|
||||
Loading…
Reference in a new issue