[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Déploiement rapide de VM Linux (Ubuntu/Debian/Fedora) via cloud-image + qemu-img + cloud-init + virt-install.
|
|
|
|
|
|
|
|
|
|
Choisissez la distribution avec --distro (ubuntu par défaut, debian, fedora)
|
|
|
|
|
et la version avec --version. « --list-images » affiche tout le catalogue et
|
|
|
|
|
les specs minimales. Reprend le workflow des notes :
|
|
|
|
|
1. Télécharge l'image cloud (si absente du cache -> pas de double téléchargement).
|
|
|
|
|
2. Convertit/copie l'image en un qcow2 de travail dédié à la VM.
|
|
|
|
|
3. Redimensionne le disque virtuel.
|
|
|
|
|
4. Génère user-data / meta-data et construit le seed.iso (cidata).
|
|
|
|
|
5. Lance virt-install en important le disque + le seed en CD-ROM.
|
|
|
|
|
|
|
|
|
|
Exemples
|
|
|
|
|
--------
|
|
|
|
|
# Le plus simple : image téléchargée automatiquement (chemin déduit de
|
|
|
|
|
# --distro/--version, mis en cache dans /var/lib/libvirt/images/iso) et
|
|
|
|
|
# outils manquants installés après confirmation.
|
|
|
|
|
sudo ./script/qemu/deploy_qemu.py --name test-vm --version 24.04 \\
|
|
|
|
|
--ssh-key ~/.ssh/id_ed25519.pub
|
|
|
|
|
|
|
|
|
|
# Debian 12 / Fedora 42 (mêmes options, --distro change la source d'image)
|
|
|
|
|
sudo ./script/qemu/deploy_qemu.py --distro debian --version 12 \\
|
|
|
|
|
--name deb12 --ssh-key ~/.ssh/id_ed25519.pub
|
|
|
|
|
sudo ./script/qemu/deploy_qemu.py --distro fedora --version 42 \\
|
|
|
|
|
--name fed42 --ssh-key ~/.ssh/id_ed25519.pub
|
|
|
|
|
|
|
|
|
|
# Voir tout le catalogue (distros, versions, specs minimales)
|
|
|
|
|
./script/qemu/deploy_qemu.py --list-images
|
|
|
|
|
|
|
|
|
|
# Télécharger (et vérifier) une image, sans créer de VM
|
|
|
|
|
sudo ./script/qemu/deploy_qemu.py --download-only --version 24.04 --verify
|
|
|
|
|
|
|
|
|
|
# Déploiement minimal en fournissant explicitement le chemin d'image
|
|
|
|
|
sudo ./qemu-deploy.py /var/lib/libvirt/images/iso/noble.img \\
|
|
|
|
|
--name test-vm --ssh-key ~/.ssh/id_ed25519.pub
|
|
|
|
|
|
|
|
|
|
# Reproduction fidèle des notes (8 Go RAM, 8 vCPU, mot de passe demandé)
|
|
|
|
|
sudo ./qemu-deploy.py /var/lib/libvirt/images/iso/noble.img \\
|
|
|
|
|
--name test-vm --memory 8192 --vcpus 8 --disk-size 120G --ask-password
|
|
|
|
|
|
|
|
|
|
# Voir ce qui serait fait, sans rien exécuter
|
|
|
|
|
./qemu-deploy.py /var/lib/libvirt/images/iso/noble.img --name test-vm --dry-run
|
|
|
|
|
|
|
|
|
|
sudo ./script/qemu/deploy_qemu.py /var/lib/libvirt/images/iso/noble.img --name test-vm --memory 8192 --vcpus 8 --disk-size 120G --ask-password --force
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
|
import getpass
|
|
|
|
|
import grp
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
import gzip
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
import hashlib
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
import shutil
|
|
|
|
|
import socket
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
import tempfile
|
|
|
|
|
import textwrap
|
|
|
|
|
import time
|
|
|
|
|
import urllib.error
|
|
|
|
|
import urllib.request
|
|
|
|
|
import warnings
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Registre des distributions : version -> (code, --osinfo, RAM min Mo, disque).
|
|
|
|
|
# Le « code » est le nom de code (Ubuntu/Debian) ou le numéro de release
|
|
|
|
|
# (Fedora). RAM/disque minimaux proviennent de libosinfo (osinfo-db) : ce sont
|
|
|
|
|
# les seuils sous lesquels virt-install avertit. Ils servent de valeurs PAR
|
|
|
|
|
# DÉFAUT — la VM démarre au plus juste sans gaspiller la RAM de l'hôte.
|
|
|
|
|
# --codename / --osinfo / --memory / --disk-size surchargent au besoin.
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# NB : les versions intermédiaires (non-LTS) sont RETIRÉES du miroir
|
|
|
|
|
# cloud-images une fois EOL (leur /current/ renvoie 404). On ne garde donc
|
|
|
|
|
# que les LTS + les intermédiaires encore publiées. À réviser au fil du temps.
|
|
|
|
|
# Disque MINIMUM = 20G partout : un ERPLibre + Odoo installé occupe ~11G, et
|
|
|
|
|
# l'installation (caches pip, sync repo, sources Odoo) en consomme plus en
|
|
|
|
|
# transitoire. Un VM à 10G tombait « Poetry installation error » (disque
|
|
|
|
|
# plein). Le qcow2 est CREUX (sparse) : une taille virtuelle plus grande ne
|
|
|
|
|
# consomme rien tant qu'elle n'est pas remplie -> 20G sans surcoût réel.
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
#
|
|
|
|
|
# 20.04 et 22.04 ne sont plus proposées : leur chaîne d'outils ne suffit plus à
|
|
|
|
|
# ERPLibre. Le mur le plus net est pikepdf, qui réclame qpdf >= 12.2, lui-même
|
|
|
|
|
# en C++20 — focal livre GCC 9. S'y ajoutaient Python 3.8 à l'amorçage, node 10,
|
|
|
|
|
# cargo 0.67 et OpenSSL 1.1.1. Chacun avait son contournement ; l'accumulation,
|
|
|
|
|
# non.
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
UBUNTU_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"24.04": ("noble", "ubuntu24.04", 3072, "20G"),
|
|
|
|
|
"25.10": ("questing", "ubuntu25.10", 3072, "20G"),
|
|
|
|
|
"26.04": ("resolute", "ubuntu26.04", 3072, "20G"),
|
|
|
|
|
}
|
|
|
|
|
DEBIAN_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"11": ("bullseye", "debian11", 1024, "20G"),
|
|
|
|
|
"12": ("bookworm", "debian12", 1024, "20G"),
|
|
|
|
|
"13": ("trixie", "debian13", 1024, "20G"),
|
|
|
|
|
}
|
|
|
|
|
FEDORA_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"41": ("41", "fedora41", 2048, "20G"),
|
|
|
|
|
"42": ("42", "fedora42", 2048, "20G"),
|
|
|
|
|
"43": ("43", "fedora43", 2048, "20G"),
|
|
|
|
|
"44": ("44", "fedora44", 2048, "20G"),
|
|
|
|
|
}
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
# Dérivés RHEL. Le second champ est l'identifiant libosinfo : « almalinux9 » et
|
|
|
|
|
# « almalinux10 » figurent dans osinfo-db, « rocky10 » n'y est pas encore sur
|
|
|
|
|
# les hôtes 24.04 — osinfo_arg() replie alors sur une valeur connue.
|
|
|
|
|
# RAM : libosinfo demande 1,5 Gio en x86_64 ; on retient 2048 comme Fedora.
|
|
|
|
|
ALMALINUX_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"9": ("9", "almalinux9", 2048, "20G"),
|
|
|
|
|
"10": ("10", "almalinux10", 2048, "20G"),
|
|
|
|
|
}
|
|
|
|
|
ROCKY_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"9": ("9", "rocky9", 2048, "20G"),
|
|
|
|
|
"10": ("10", "rocky10", 2048, "20G"),
|
|
|
|
|
}
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
# openSUSE, deux produits distincts et pas deux versions du même.
|
|
|
|
|
# Leap NUMÉROTÉ, base SLE, ~2 ans de support. C'est le défaut, et le
|
|
|
|
|
# seul des deux à proposer pour une installation qui doit durer.
|
|
|
|
|
# Tumbleweed ROLLING, sans numéro. Gardé comme banc d'essai des ruptures à
|
|
|
|
|
# venir, pas comme cible. Sa dérive d'instantanés est réelle : deux
|
|
|
|
|
# VM déployées le même jour ont vu git-daemon 2.54 sur s390x et
|
|
|
|
|
# 2.55 sur amd64, et l'image livrée est toujours en retard sur les
|
|
|
|
|
# dépôts, d'où le « zypper dup » obligatoire avant toute install.
|
|
|
|
|
#
|
|
|
|
|
# Les deux dépassent le seuil qpdf de pikepdf : aucune compilation de qpdf, ce
|
|
|
|
|
# qui change tout sous émulation s390x.
|
|
|
|
|
#
|
|
|
|
|
# osinfo : « opensuse16.0 » n'est pas encore dans osinfo-db, qui s'arrête à
|
|
|
|
|
# 15.6. osinfo_arg() replie sur le DERNIER id connu de la table — d'où l'ordre,
|
|
|
|
|
# Tumbleweed en second servant de repli à Leap.
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
OPENSUSE_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
"16.0": ("16.0", "opensuse16.0", 2048, "20G"),
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
"tumbleweed": ("tumbleweed", "opensusetumbleweed", 2048, "20G"),
|
|
|
|
|
}
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
# Arch est en rolling release : une seule « version » (latest).
|
|
|
|
|
ARCH_VERSIONS: dict[str, tuple[str, str, int, str]] = {
|
|
|
|
|
"latest": ("latest", "archlinux", 1024, "20G"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 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"),
|
|
|
|
|
"debian": (DEBIAN_VERSIONS, "12"),
|
|
|
|
|
"fedora": (FEDORA_VERSIONS, "42"),
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
"almalinux": (ALMALINUX_VERSIONS, "9"),
|
|
|
|
|
"rocky": (ROCKY_VERSIONS, "10"),
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
"opensuse": (OPENSUSE_VERSIONS, "16.0"),
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
"arch": (ARCH_VERSIONS, "latest"),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Traduction de l'arch générique (amd64/arm64) vers le nom propre à la distro.
|
|
|
|
|
# s390x s'écrit « s390x » partout (identité), donc aucune entrée n'est requise.
|
|
|
|
|
ARCH_ALIASES: dict[str, dict[str, str]] = {
|
|
|
|
|
"fedora": {"amd64": "x86_64", "arm64": "aarch64"},
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
"almalinux": {"amd64": "x86_64", "arm64": "aarch64"},
|
|
|
|
|
"rocky": {"amd64": "x86_64", "arm64": "aarch64"},
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
"opensuse": {"amd64": "x86_64", "arm64": "aarch64"},
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
"arch": {"amd64": "x86_64", "arm64": "aarch64"},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Architectures non-x86 nécessitant une machine/un amorçage spécifiques (voir
|
|
|
|
|
# virt_install). Émulées en TCG (pas de KVM), donc LENTES, quand elles
|
|
|
|
|
# diffèrent de l'architecture de l'hôte.
|
|
|
|
|
NON_X86_ARCHES: tuple[str, ...] = ("arm64", "s390x")
|
|
|
|
|
|
|
|
|
|
# Distros publiant des images cloud par architecture (vérifié juillet 2026) :
|
|
|
|
|
# - s390x (IBM Z) : Ubuntu seulement (Debian/Fedora : 404 ; Arch : x86/arm).
|
|
|
|
|
# - arm64/aarch64 : Ubuntu, Debian, Fedora (Arch : pas d'image cloud officielle
|
|
|
|
|
# aarch64 sur geo.mirror.pkgbuild.com).
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
S390X_DISTROS: tuple[str, ...] = (
|
|
|
|
|
"ubuntu",
|
|
|
|
|
"almalinux",
|
|
|
|
|
"rocky",
|
|
|
|
|
"fedora",
|
|
|
|
|
"opensuse",
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
# Debian n'a PAS d'image cloud s390x, et n'en aura pas par cette voie :
|
|
|
|
|
# vérifié sur cloud.debian.org, les arborescences bookworm et trixie ne
|
|
|
|
|
# publient que amd64, arm64, ppc64el et riscv64. Le port s390x existe
|
|
|
|
|
# pourtant — « binary-s390x » répond 200 — et debian-installer livre
|
|
|
|
|
# kernel + initrd pour les deux versions. On y passe donc par
|
|
|
|
|
# l'INSTALLATEUR au lieu d'un qcow2 tout fait : voir uses_installer().
|
|
|
|
|
"debian",
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
)
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
|
|
|
|
|
# Une distro peut ne publier qu'une PARTIE de ses versions sur une
|
|
|
|
|
# architecture. Déclaration POSITIVE : hors de cette table, toutes les versions
|
|
|
|
|
# du catalogue sont réputées disponibles.
|
|
|
|
|
#
|
|
|
|
|
# Fedora ne construit s390x que pour la version courante, et sur une
|
|
|
|
|
# arborescence à part (« fedora-secondary ») : la 41 et la 42 y sont retirées
|
|
|
|
|
# (404 sur le miroir maître), la 44 n'est pour l'instant que sur certains
|
|
|
|
|
# miroirs tiers. Seule la 43 est servie par dl.fedoraproject.org — vérifié.
|
|
|
|
|
ARCH_ONLY_VERSIONS: dict[str, dict[str, tuple[str, ...]]] = {
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
# Debian sur s390x passe par debian-installer, dont les images sont
|
|
|
|
|
# publiées pour bookworm et trixie — vérifié. bullseye est écartée : elle
|
|
|
|
|
# est en fin de vie et son installateur n'a pas été éprouvé ici.
|
|
|
|
|
"s390x": {"fedora": ("43",), "debian": ("12", "13")},
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
}
|
|
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
# Distros installées par debian-installer plutôt que depuis une image cloud.
|
|
|
|
|
# La différence n'est pas cosmétique : pas de qcow2 à convertir, pas de seed
|
|
|
|
|
# cloud-init, un disque VIERGE et un amorçage kernel+initrd.
|
|
|
|
|
INSTALLER_COMBOS: tuple[tuple[str, str], ...] = (("debian", "s390x"),)
|
|
|
|
|
|
[FIX] todo qemu : lire les distros par arch dans deploy_qemu
L'ecran de deploiement portait sa PROPRE copie de S390X_DISTROS, sous un
commentaire promettant qu'elle restait « coherente avec deploy_qemu.py ».
Une copie ne tient aucune promesse : Debian a gagne s390x la-bas et
l'ecran ne le proposait toujours pas.
_qemu_arch_distros lit desormais ARCH_DISTRO_SUPPORT, et _qemu_arches_for
y passe aussi pour que « toutes les architectures » n'offre rien que
deploy_qemu refuserait ensuite. Les tuples locaux restent en repli si
l'import echoue. L'import est memorise : le catalogue l'interrogeait une
fois par couple (distro, version).
Plancher memoire de 2048 Mio sur la voie installateur, annonce et jamais
abaissant : d-i deplie un systeme de fichiers en RAM la ou une image
cloud arrive installee, et le manque s'y voit comme un ecran fige.
--- EN ---
The deploy screen carried its OWN copy of S390X_DISTROS, under a comment
promising it stayed "consistent with deploy_qemu.py". A copy keeps no
promise: Debian gained s390x over there and the screen still did not
offer it.
_qemu_arch_distros now reads ARCH_DISTRO_SUPPORT, and _qemu_arches_for
goes through it too, so "all architectures" offers nothing deploy_qemu
would later refuse. The local tuples remain as a fallback if the import
fails. The import is memoised: the catalogue queried it once per
(distro, version) pair.
A 2048 MiB memory floor on the installer path, announced and never
lowering: d-i unpacks a filesystem into RAM where a cloud image arrives
installed, and running short shows up as a frozen screen.
Assisted-by: Claude Opus 5
(cherry picked from commit f0ee70c1ac43ba2bc9f5fb78fe71f821a15daf7d)
2026-08-13 16:44:27 -04:00
|
|
|
# Plancher mémoire de l'installateur : il déplie un système de fichiers entier
|
|
|
|
|
# en RAM, là où une image cloud arrive déjà installée.
|
|
|
|
|
INSTALLER_MIN_RAM = 2048
|
|
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
# kernel.debian / initrd.debian du port s390x. « current » suit les mises à
|
|
|
|
|
# jour de l'installateur sans figer un numéro qui périmerait.
|
|
|
|
|
INSTALLER_URL = (
|
|
|
|
|
"https://deb.debian.org/debian/dists/{code}/main/installer-s390x"
|
|
|
|
|
"/current/images/generic/{fichier}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def uses_installer(distro: str, arch: str) -> bool:
|
|
|
|
|
"""Vrai si cette combinaison s'installe par debian-installer."""
|
|
|
|
|
return (distro, arch) in INSTALLER_COMBOS
|
|
|
|
|
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
|
|
|
|
|
def arch_versions(distro: str, arch: str, versions) -> list[str]:
|
|
|
|
|
"""Versions de `distro` réellement publiées pour `arch`."""
|
|
|
|
|
only = ARCH_ONLY_VERSIONS.get(arch, {}).get(distro)
|
|
|
|
|
return [v for v in versions if only is None or v in only]
|
|
|
|
|
|
|
|
|
|
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
ARM64_DISTROS: tuple[str, ...] = (
|
|
|
|
|
"ubuntu",
|
|
|
|
|
"debian",
|
|
|
|
|
"fedora",
|
|
|
|
|
"almalinux",
|
|
|
|
|
"rocky",
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
"opensuse",
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
)
|
[REM] install: drop Ubuntu 20.04 and 22.04
Two blockers on 20.04, neither worth carrying. "TypeError: unsupported
operand type(s) for |" fired before the install even started:
update_env_version.py imports erplibre_state as the very first thing
"make install_os" does, hence before pyenv, on the system Python 3.8,
where a "str | None" annotation is evaluated at import. And cryptography
now demands OpenSSL 3, while focal ships 1.1.1.
Annotations are therefore deferred, a contract the bootstrap file already
stated in a comment and now holds across script/version and
script/install. But the releases themselves go: pikepdf requires
qpdf >= 12.2, compiled in C++20, when focal ships GCC 9.
--- FR ---
Deux blocages sur 20.04, aucun ne méritant d'être porté. « TypeError:
unsupported operand type(s) for | » se déclenchait avant même le début de
l'installation : update_env_version.py importe erplibre_state en tout
premier lieu de « make install_os », donc avant pyenv, sur le Python 3.8
du système, où une annotation « str | None » est évaluée à l'import. Et
cryptography exige désormais OpenSSL 3, quand focal livre 1.1.1.
Les annotations sont donc différées, contrat que le fichier d'amorçage
énonçait déjà en commentaire et qui tient maintenant sur script/version et
script/install. Mais les versions elles-mêmes partent : pikepdf réclame
qpdf >= 12.2, compilé en C++20, quand focal livre GCC 9.
Assisted-by: Claude Opus 5
2026-08-11 08:03:32 -04:00
|
|
|
|
|
|
|
|
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
# arch générique -> distros la publiant (pour valider --arch tôt).
|
|
|
|
|
ARCH_DISTRO_SUPPORT: dict[str, tuple[str, ...]] = {
|
|
|
|
|
"s390x": S390X_DISTROS,
|
|
|
|
|
"arm64": ARM64_DISTROS,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def host_arch() -> str:
|
|
|
|
|
"""Architecture de l'hôte en jeton générique (amd64/arm64/s390x)."""
|
|
|
|
|
try:
|
|
|
|
|
machine = os.uname().machine
|
|
|
|
|
except (AttributeError, OSError):
|
|
|
|
|
machine = ""
|
|
|
|
|
return {
|
|
|
|
|
"x86_64": "amd64",
|
|
|
|
|
"amd64": "amd64",
|
|
|
|
|
"aarch64": "arm64",
|
|
|
|
|
"arm64": "arm64",
|
|
|
|
|
"s390x": "s390x",
|
|
|
|
|
}.get(machine, "amd64")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ARCH_CLOUD_BASE = "https://geo.mirror.pkgbuild.com/images/latest"
|
|
|
|
|
|
|
|
|
|
CLOUD_IMG_BASE = "https://cloud-images.ubuntu.com"
|
|
|
|
|
# Debian : cloud.debian.org est un redirecteur qui, selon le réseau, peut
|
|
|
|
|
# renvoyer vers un miroir injoignable. On essaie donc plusieurs bases dans
|
|
|
|
|
# l'ordre (le premier miroir qui répond gagne). Les deux acc.umu.se sont des
|
|
|
|
|
# miroirs Debian officiels de repli.
|
|
|
|
|
DEBIAN_CLOUD_BASES: tuple[str, ...] = (
|
|
|
|
|
"https://cloud.debian.org/images/cloud",
|
|
|
|
|
"https://gemmei.ftp.acc.umu.se/cdimage/cloud",
|
|
|
|
|
"https://laotzu.ftp.acc.umu.se/cdimage/cloud",
|
|
|
|
|
)
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
ALMALINUX_CLOUD_BASE = "https://repo.almalinux.org/almalinux"
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
OPENSUSE_BASE = "https://download.opensuse.org"
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
ROCKY_CLOUD_BASE = "https://dl.rockylinux.org/pub/rocky"
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
FEDORA_BASE = "https://download.fedoraproject.org/pub/fedora/linux/releases"
|
|
|
|
|
# Serveur MAÎTRE (pas de redirection MirrorManager) : repli fiable quand le
|
|
|
|
|
# redirecteur envoie sur un miroir incomplet (fréquent en déploiement
|
|
|
|
|
# PARALLÈLE : chaque requête peut tomber sur un miroir différent/désynchronisé).
|
|
|
|
|
FEDORA_BASE_MASTER = "https://dl.fedoraproject.org/pub/fedora/linux/releases"
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
# s390x est une architecture SECONDAIRE chez Fedora : ses images ne sont PAS
|
|
|
|
|
# sous /pub/fedora/linux/ mais sous /pub/fedora-secondary/, et seule la
|
|
|
|
|
# version courante y est construite.
|
|
|
|
|
FEDORA_SECONDARY = (
|
|
|
|
|
"https://download.fedoraproject.org/pub/fedora-secondary/releases"
|
|
|
|
|
)
|
|
|
|
|
FEDORA_SECONDARY_MASTER = (
|
|
|
|
|
"https://dl.fedoraproject.org/pub/fedora-secondary/releases"
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
# Répertoire de cache par défaut des images cloud (cohérent avec --disk-dir /
|
|
|
|
|
# --seed-dir). L'écriture y nécessite root : le déploiement tourne de toute
|
|
|
|
|
# façon sous sudo (virt-install). Surchargez avec --image-dir au besoin.
|
|
|
|
|
DEFAULT_IMAGE_DIR = Path("/var/lib/libvirt/images/iso")
|
|
|
|
|
|
|
|
|
|
# Emplacements de la base osinfo-db (détection d'un --osinfo connu).
|
|
|
|
|
OSINFO_DB_DIRS: tuple[str, ...] = (
|
|
|
|
|
"/usr/share/osinfo/os",
|
|
|
|
|
"/usr/local/share/osinfo/os",
|
|
|
|
|
os.path.expanduser("~/.local/share/osinfo/os"),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def distro_arch(distro: str, arch: str) -> str:
|
|
|
|
|
"""Nom d'architecture attendu par la distro (Fedora utilise x86_64)."""
|
|
|
|
|
return ARCH_ALIASES.get(distro, {}).get(arch, arch)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def image_url(distro: str, code: str, arch: str, version: str) -> str:
|
|
|
|
|
"""URL directe (primaire) de l'image cloud (Ubuntu/Debian)."""
|
|
|
|
|
return image_candidates(distro, code, arch, version, dry_run=True)[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def image_candidates(
|
|
|
|
|
distro: str, code: str, arch: str, version: str, dry_run: bool = False
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
"""Liste ordonnée d'URL candidates pour l'image cloud. Plusieurs miroirs
|
|
|
|
|
pour Debian ; une seule URL pour Ubuntu/Fedora."""
|
|
|
|
|
a = distro_arch(distro, arch)
|
|
|
|
|
if distro == "ubuntu":
|
|
|
|
|
# /releases/<version>/release/ : présent pour toutes les versions
|
|
|
|
|
# publiées (redirige vers l'image datée), contrairement à /current/
|
|
|
|
|
# qui disparaît quand une version intermédiaire devient EOL.
|
|
|
|
|
return [
|
|
|
|
|
f"{CLOUD_IMG_BASE}/releases/{version}/release/"
|
|
|
|
|
f"ubuntu-{version}-server-cloudimg-{a}.img"
|
|
|
|
|
]
|
|
|
|
|
if distro == "debian":
|
|
|
|
|
return [
|
|
|
|
|
f"{base}/{code}/latest/debian-{version}-genericcloud-{a}.qcow2"
|
|
|
|
|
for base in DEBIAN_CLOUD_BASES
|
|
|
|
|
]
|
|
|
|
|
if distro == "fedora":
|
|
|
|
|
return [resolve_fedora_url(version, arch, dry_run)]
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
if distro == "almalinux":
|
|
|
|
|
# Contrairement à Fedora, AlmaLinux publie un lien « latest » STABLE
|
|
|
|
|
# par majeure et par architecture : aucun index HTML à analyser. On
|
|
|
|
|
# vise l'arbre de la MAJEURE (9/, 10/), qui suit la mineure courante —
|
|
|
|
|
# les répertoires mineurs périmés sont retirés du miroir.
|
|
|
|
|
return [
|
|
|
|
|
f"{ALMALINUX_CLOUD_BASE}/{version}/cloud/{a}/images/"
|
|
|
|
|
f"AlmaLinux-{version}-GenericCloud-latest.{a}.qcow2"
|
|
|
|
|
]
|
|
|
|
|
if distro == "rocky":
|
|
|
|
|
# Même principe : l'alias « .latest » suit le point release courant, et
|
|
|
|
|
# les précédents partent au vault — une URL figée casserait.
|
|
|
|
|
return [
|
|
|
|
|
f"{ROCKY_CLOUD_BASE}/{version}/images/{a}/"
|
|
|
|
|
f"Rocky-{version}-GenericCloud.latest.{a}.qcow2"
|
|
|
|
|
]
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
if distro == "opensuse":
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
# zsystems DOUBLE l'architecture dans le nom du fichier — irrégularité
|
|
|
|
|
# vérifiée dans l'index, pas déduite : « .s390x-s390x-Cloud » contre
|
|
|
|
|
# « .x86_64-Cloud ». Elle vaut pour les DEUX produits.
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
tag = f"{a}-{a}" if arch == "s390x" else a
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
if version == "tumbleweed":
|
|
|
|
|
# Tumbleweed sépare les architectures secondaires sous /ports/.
|
|
|
|
|
port = {"s390x": "ports/zsystems/", "arm64": "ports/aarch64/"}.get(
|
|
|
|
|
arch, ""
|
|
|
|
|
)
|
|
|
|
|
return [
|
|
|
|
|
f"{OPENSUSE_BASE}/{port}tumbleweed/appliances/"
|
|
|
|
|
f"openSUSE-Tumbleweed-Minimal-VM.{tag}-Cloud.qcow2"
|
|
|
|
|
]
|
|
|
|
|
# Leap, lui, publie TOUTES les architectures dans un seul répertoire :
|
|
|
|
|
# les chemins /ports/ équivalents rendent 404. x86_64, aarch64 et s390x
|
|
|
|
|
# y sont côte à côte (relevé dans l'index de 16.0, les trois en 200).
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
return [
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
f"{OPENSUSE_BASE}/distribution/leap/{version}/appliances/"
|
|
|
|
|
f"Leap-{version}-Minimal-VM.{tag}-Cloud.qcow2"
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
]
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
if distro == "arch":
|
|
|
|
|
# Rolling release : image « latest » officielle (cloud-init inclus).
|
|
|
|
|
return [f"{ARCH_CLOUD_BASE}/Arch-Linux-{a}-cloudimg.qcow2"]
|
|
|
|
|
raise ValueError(f"URL indisponible pour la distro {distro!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_fedora_url(version: str, arch: str, dry_run: bool) -> str:
|
|
|
|
|
"""Résout l'URL du qcow2 « Fedora Cloud Base Generic » depuis l'index
|
|
|
|
|
HTML des releases (Fedora ne publie pas de lien « latest »). ROBUSTE :
|
|
|
|
|
plusieurs tentatives sur le redirecteur puis repli sur le serveur maître
|
|
|
|
|
(dl.fedoraproject.org) — sinon, en déploiement parallèle, une requête
|
|
|
|
|
tombée sur un miroir incomplet faisait échouer la VM (« image introuvable »)
|
|
|
|
|
alors que l'image existe bel et bien."""
|
|
|
|
|
a = distro_arch("fedora", arch)
|
|
|
|
|
pattern = re.compile(
|
|
|
|
|
rf"Fedora-Cloud-Base-Generic-{version}-[0-9.]+\.{a}\.qcow2"
|
|
|
|
|
)
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
# Architecture secondaire (s390x, ppc64le) : autre arborescence.
|
|
|
|
|
primary, master = (
|
|
|
|
|
(FEDORA_SECONDARY, FEDORA_SECONDARY_MASTER)
|
|
|
|
|
if arch == "s390x"
|
|
|
|
|
else (FEDORA_BASE, FEDORA_BASE_MASTER)
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
if dry_run:
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
index = f"{primary}/{version}/Cloud/{a}/images/"
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
return index + f"Fedora-Cloud-Base-Generic-{version}-<build>.{a}.qcow2"
|
|
|
|
|
# On essaie le redirecteur (2 fois : chaque requête peut viser un miroir
|
|
|
|
|
# différent) puis le serveur maître (toujours complet).
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
bases = [primary, primary, master]
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
last_err = ""
|
|
|
|
|
for base in bases:
|
|
|
|
|
index = f"{base}/{version}/Cloud/{a}/images/"
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(
|
|
|
|
|
index, timeout=30
|
|
|
|
|
) as resp: # noqa: S310
|
|
|
|
|
html = resp.read().decode(errors="replace")
|
|
|
|
|
except Exception as exc: # pragma: no cover - dépend du réseau
|
|
|
|
|
last_err = str(exc)
|
|
|
|
|
continue
|
|
|
|
|
names = sorted(set(pattern.findall(html)))
|
|
|
|
|
if names:
|
|
|
|
|
return index + names[-1]
|
|
|
|
|
sys.exit(
|
|
|
|
|
"Aucune image « Fedora-Cloud-Base-Generic » trouvée pour "
|
|
|
|
|
f"Fedora {version} ({a}) après plusieurs miroirs"
|
|
|
|
|
+ (f" (dernière erreur : {last_err})" if last_err else "")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def default_image_name(distro: str, code: str, arch: str, version: str) -> str:
|
|
|
|
|
"""Nom de fichier local pour le cache d'image."""
|
|
|
|
|
a = distro_arch(distro, arch)
|
|
|
|
|
if distro == "ubuntu":
|
|
|
|
|
return f"ubuntu-{version}-server-cloudimg-{a}.img"
|
|
|
|
|
if distro == "debian":
|
|
|
|
|
return f"debian-{version}-genericcloud-{a}.qcow2"
|
|
|
|
|
if distro == "arch":
|
|
|
|
|
return f"arch-linux-{a}-cloudimg.qcow2"
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
if distro == "opensuse":
|
[ADD] qemu: openSUSE Leap 16.0, numbered, as the default
The catalogue only offered openSUSE Tumbleweed, a rolling release with
no version number. Nearly every openSUSE incident in this series came
from that: the cloud image is a snapshot lagging behind its own repos,
hence the mandatory "zypper dup", and two VMs deployed the same day saw
git-daemon 2.54 on s390x against 2.55 on amd64. For an ERP platform,
that is not what should be offered by default.
Leap 16.0 is numbered, stable, and publishes the three architectures
that matter -- verified, x86_64, aarch64 and s390x all 200. It also
unifies its tree: no separate /ports/, unlike Tumbleweed, whose
equivalent paths 404 for Leap.
Tumbleweed stays on offer, as a bellwether for breakage to come. Two
things separate them in the scripts: the repository path, and "dup" --
which on Leap means CHANGING version, not updating.
--- FR ---
Le catalogue n'offrait qu'openSUSE Tumbleweed, une rolling sans numéro
de version. Presque tous les incidents openSUSE de la série venaient de
là : l'image cloud est un instantané en retard sur ses dépôts, d'où le
« zypper dup » obligatoire, et deux VM déployées le même jour ont vu
git-daemon 2.54 sur s390x contre 2.55 sur amd64. Pour une plateforme
ERP, ce n'est pas ce qu'on veut proposer par défaut.
Leap 16.0 est numérotée, stable, et publie les trois architectures qui
comptent — vérifié, x86_64, aarch64 et s390x en 200. Elle unifie de plus
son arbre : pas de /ports/ séparé, contrairement à Tumbleweed, dont les
chemins équivalents rendent 404 pour Leap.
Tumbleweed reste offerte, comme banc d'essai des ruptures à venir.
Deux points les séparent dans les scripts : le chemin des dépôts, et
« dup » — qui sur Leap sert à CHANGER de version, pas à mettre à jour.
Assisted-by: Claude Opus 5
2026-08-12 01:16:17 -04:00
|
|
|
if version == "tumbleweed":
|
|
|
|
|
return f"opensuse-tumbleweed-minimal-vm-{a}.qcow2"
|
|
|
|
|
return f"opensuse-leap-{version}-minimal-vm-{a}.qcow2"
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
if distro in ("almalinux", "rocky"):
|
|
|
|
|
# « latest » est MUTABLE : le nom de cache porte donc la majeure, et
|
|
|
|
|
# une image déjà téléchargée sera réutilisée telle quelle. C'est voulu
|
|
|
|
|
# (reproductibilité d'un déploiement à l'autre) ; supprimer le fichier
|
|
|
|
|
# du cache suffit à repartir sur le build courant.
|
|
|
|
|
return f"{distro}-{version}-genericcloud-{a}.qcow2"
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
return f"fedora-cloud-{version}-{a}.qcow2"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def osinfo_known(short_id: str) -> bool:
|
|
|
|
|
"""Vrai si l'id osinfo (ex. « ubuntu26.04 ») figure dans osinfo-db."""
|
|
|
|
|
needle = f"<short-id>{short_id}</short-id>"
|
|
|
|
|
for base in OSINFO_DB_DIRS:
|
|
|
|
|
if not os.path.isdir(base):
|
|
|
|
|
continue
|
|
|
|
|
for root, _dirs, files in os.walk(base):
|
|
|
|
|
for fn in files:
|
|
|
|
|
if not fn.endswith(".xml"):
|
|
|
|
|
continue
|
|
|
|
|
try:
|
|
|
|
|
with open(
|
|
|
|
|
os.path.join(root, fn),
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
errors="replace",
|
|
|
|
|
) as fh:
|
|
|
|
|
if needle in fh.read():
|
|
|
|
|
return True
|
|
|
|
|
except OSError:
|
|
|
|
|
continue
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def osinfo_arg(osinfo: str, distro: str = "") -> str:
|
|
|
|
|
"""Valeur --osinfo résiliente à un osinfo-db périmé. Si l'id est connu on
|
|
|
|
|
l'utilise ; sinon on retombe sur le DERNIER osinfo connu de la même distro
|
|
|
|
|
(meilleures perfs que « generic », pas d'avertissement) ; en dernier
|
|
|
|
|
recours, détection best-effort. Utile pour une version plus récente que la
|
|
|
|
|
base locale (ex. ubuntu26.04, fedora43+)."""
|
|
|
|
|
if "=" in osinfo or "," in osinfo:
|
|
|
|
|
return osinfo # forme avancée déjà fournie par l'utilisateur
|
|
|
|
|
if osinfo_known(osinfo):
|
|
|
|
|
return osinfo
|
|
|
|
|
# Repli 1 : dernier osinfo connu de la même distro (ordre de la table).
|
|
|
|
|
versions = DISTROS.get(distro, ({}, ""))[0]
|
|
|
|
|
known = [v[1] for v in versions.values() if osinfo_known(v[1])]
|
|
|
|
|
if known:
|
|
|
|
|
fallback = known[-1]
|
|
|
|
|
print(
|
|
|
|
|
f" osinfo « {osinfo} » inconnu (osinfo-db) — repli sur "
|
|
|
|
|
f"« {fallback} » (proche). « sudo apt upgrade osinfo-db » pour "
|
|
|
|
|
"l'entrée exacte."
|
|
|
|
|
)
|
|
|
|
|
return fallback
|
|
|
|
|
# Repli 2 : détection best-effort (évite l'échec, peut donner generic).
|
|
|
|
|
print(
|
|
|
|
|
f" osinfo « {osinfo} » inconnu de la base locale (osinfo-db) — repli "
|
|
|
|
|
"sur la détection auto (detect=on,require=off).\n"
|
|
|
|
|
" Astuce : « sudo apt upgrade osinfo-db » pour des métadonnées à jour."
|
|
|
|
|
)
|
|
|
|
|
return "detect=on,require=off"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def list_images() -> None:
|
|
|
|
|
"""Affiche toutes les distros/versions et leurs specs (--list-images)."""
|
|
|
|
|
print("Images cloud disponibles (distro / version / specs) :\n")
|
|
|
|
|
for distro, (versions, default) in DISTROS.items():
|
|
|
|
|
print(f" {distro} (défaut : {default})")
|
|
|
|
|
for v, (code, osinfo, ram, disk) in versions.items():
|
|
|
|
|
star = "*" if v == default else " "
|
|
|
|
|
note = (
|
|
|
|
|
""
|
|
|
|
|
if osinfo_known(osinfo)
|
|
|
|
|
else " [osinfo local absent → auto]"
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
f" {star} {v:<7} {code:<10} osinfo={osinfo:<12} "
|
|
|
|
|
f"RAM≥{ram}Mo disque≥{disk}{note}"
|
|
|
|
|
)
|
|
|
|
|
print()
|
|
|
|
|
print("Exemple : deploy_qemu.py --distro debian --version 12 --name vm1")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Utilitaires d'exécution
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
class Runner:
|
|
|
|
|
"""Exécute (ou affiche, en dry-run) les commandes, avec sudo au besoin."""
|
|
|
|
|
|
|
|
|
|
def __init__(self, use_sudo: bool, dry_run: bool) -> None:
|
|
|
|
|
self.use_sudo = use_sudo
|
|
|
|
|
self.dry_run = dry_run
|
|
|
|
|
|
|
|
|
|
def run(
|
|
|
|
|
self, cmd: list[str], *, privileged: bool = False, check: bool = True
|
|
|
|
|
) -> None:
|
|
|
|
|
if privileged and self.use_sudo:
|
|
|
|
|
cmd = ["sudo", *cmd]
|
|
|
|
|
printable = " ".join(cmd)
|
|
|
|
|
if self.dry_run:
|
|
|
|
|
print(f" [dry-run] {printable}")
|
|
|
|
|
return
|
|
|
|
|
print(f" $ {printable}")
|
|
|
|
|
try:
|
|
|
|
|
subprocess.run(cmd, check=check)
|
|
|
|
|
except subprocess.CalledProcessError as exc:
|
|
|
|
|
# Sortie propre plutôt qu'une trace Python illisible.
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"\nÉchec de la commande (code {exc.returncode}) :\n"
|
|
|
|
|
f" {printable}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def need_tool(name: str) -> None:
|
|
|
|
|
if shutil.which(name) is None:
|
|
|
|
|
sys.exit(f"Erreur : outil requis introuvable dans le PATH : {name!r}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Détection et installation des dépendances système
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Outils indispensables au déploiement complet (le mode --download-only n'en
|
|
|
|
|
# requiert aucun).
|
|
|
|
|
# URI libvirt visée par TOUS les clients (virsh, virt-install). À ne jamais
|
|
|
|
|
# laisser implicite : pour un utilisateur non root, libvirt choisit
|
|
|
|
|
# « qemu:///session », où le réseau « default » N'EXISTE PAS — virt-install
|
|
|
|
|
# échoue alors sur « --network network=default » de façon incompréhensible.
|
|
|
|
|
# Appartenir au groupe libvirt donne le DROIT d'accéder à qemu:///system mais
|
|
|
|
|
# ne change PAS l'URI par défaut : il faut l'imposer.
|
|
|
|
|
LIBVIRT_URI = "qemu:///system"
|
|
|
|
|
|
|
|
|
|
REQUIRED_TOOLS: tuple[str, ...] = ("qemu-img", "virt-install", "virsh")
|
|
|
|
|
# Pour construire le seed cloud-init il faut AU MOINS un de ces outils.
|
|
|
|
|
SEED_TOOLS: tuple[str, ...] = ("cloud-localds", "genisoimage")
|
|
|
|
|
|
|
|
|
|
# outil -> nom de paquet, selon le gestionnaire de paquets détecté.
|
|
|
|
|
TOOL_PACKAGES: dict[str, dict[str, str]] = {
|
|
|
|
|
"apt": {
|
|
|
|
|
"qemu-img": "qemu-utils",
|
|
|
|
|
"virt-install": "virtinst",
|
|
|
|
|
"virsh": "libvirt-clients",
|
|
|
|
|
"cloud-localds": "cloud-image-utils",
|
|
|
|
|
"genisoimage": "genisoimage",
|
|
|
|
|
"openssl": "openssl",
|
|
|
|
|
},
|
|
|
|
|
"dnf": {
|
|
|
|
|
"qemu-img": "qemu-img",
|
|
|
|
|
"virt-install": "virt-install",
|
|
|
|
|
"virsh": "libvirt-client",
|
|
|
|
|
"cloud-localds": "cloud-utils",
|
|
|
|
|
"genisoimage": "genisoimage",
|
|
|
|
|
"openssl": "openssl",
|
|
|
|
|
},
|
|
|
|
|
"pacman": {
|
|
|
|
|
"qemu-img": "qemu-img",
|
|
|
|
|
"virt-install": "virt-install",
|
|
|
|
|
"virsh": "libvirt",
|
|
|
|
|
"cloud-localds": "cloud-image-utils",
|
|
|
|
|
"genisoimage": "cdrtools",
|
|
|
|
|
"openssl": "openssl",
|
|
|
|
|
},
|
|
|
|
|
"zypper": {
|
|
|
|
|
"qemu-img": "qemu-tools",
|
|
|
|
|
"virt-install": "virt-install",
|
|
|
|
|
"virsh": "libvirt-client",
|
|
|
|
|
"cloud-localds": "cloud-utils-cloud-localds",
|
|
|
|
|
"genisoimage": "genisoimage",
|
|
|
|
|
"openssl": "openssl",
|
|
|
|
|
},
|
|
|
|
|
"brew": {
|
|
|
|
|
"qemu-img": "qemu",
|
|
|
|
|
"virt-install": "virt-manager",
|
|
|
|
|
"virsh": "libvirt",
|
|
|
|
|
"cloud-localds": "cdrtools",
|
|
|
|
|
"genisoimage": "cdrtools",
|
|
|
|
|
"openssl": "openssl",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Paquets fournissant le démon libvirt + l'émulateur QEMU système. Ils sont
|
|
|
|
|
# INDISPENSABLES pour exécuter la VM : virsh/virt-install (clients) seuls ne
|
|
|
|
|
# créent pas le socket /var/run/libvirt/libvirt-sock.
|
|
|
|
|
# On inclut le firmware UEFI (OVMF/edk2) : le boot par défaut est UEFI
|
|
|
|
|
# (indispensable pour Debian 13+ ; les images récentes n'ont plus de BIOS).
|
|
|
|
|
DAEMON_PACKAGES: dict[str, list[str]] = {
|
|
|
|
|
"apt": ["libvirt-daemon-system", "qemu-system-x86", "ovmf"],
|
|
|
|
|
"dnf": ["libvirt-daemon-kvm", "qemu-kvm", "edk2-ovmf"],
|
|
|
|
|
"pacman": ["libvirt", "qemu-desktop", "dnsmasq", "edk2-ovmf"],
|
|
|
|
|
"zypper": [
|
|
|
|
|
"libvirt-daemon",
|
|
|
|
|
"libvirt-daemon-qemu",
|
|
|
|
|
"qemu-kvm",
|
|
|
|
|
"qemu-ovmf-x86_64",
|
|
|
|
|
],
|
|
|
|
|
"brew": [],
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Émulateurs QEMU système pour architectures non-x86, requis pour --arch
|
|
|
|
|
# <arch> sur un hôte d'architecture différente (émulation TCG). Par arch puis
|
|
|
|
|
# par gestionnaire de paquets. NB apt : qemu-system-misc ne contient PAS s390x
|
|
|
|
|
# (alpha/avr/… seulement) -> paquet dédié qemu-system-s390x ; qemu-system-arm
|
|
|
|
|
# fournit qemu-system-aarch64 ; l'arm64 exige aussi le firmware UEFI AAVMF.
|
|
|
|
|
EMULATOR_PACKAGES: dict[str, dict[str, list[str]]] = {
|
|
|
|
|
"s390x": {
|
|
|
|
|
"apt": ["qemu-system-s390x"],
|
|
|
|
|
"dnf": ["qemu-system-s390x"],
|
|
|
|
|
"pacman": ["qemu-emulators-full"],
|
|
|
|
|
"zypper": ["qemu-s390"],
|
|
|
|
|
"brew": [],
|
|
|
|
|
},
|
|
|
|
|
"arm64": {
|
|
|
|
|
"apt": ["qemu-system-arm", "qemu-efi-aarch64"],
|
|
|
|
|
"dnf": ["qemu-system-aarch64", "edk2-aarch64"],
|
|
|
|
|
"pacman": ["qemu-emulators-full", "edk2-aarch64"],
|
|
|
|
|
"zypper": ["qemu-arm", "qemu-uefi-aarch64"],
|
|
|
|
|
"brew": [],
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Binaire émulateur par arch (détection de présence).
|
|
|
|
|
EMULATOR_BINARY: dict[str, str] = {
|
|
|
|
|
"s390x": "qemu-system-s390x",
|
|
|
|
|
"arm64": "qemu-system-aarch64",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Firmwares UEFI AAVMF possibles (arm64) : au moins un doit exister.
|
|
|
|
|
AARCH64_FIRMWARE_PATHS: tuple[str, ...] = (
|
|
|
|
|
"/usr/share/AAVMF/AAVMF_CODE.fd",
|
|
|
|
|
"/usr/share/AAVMF/AAVMF_CODE.no-secboot.fd",
|
|
|
|
|
"/usr/share/edk2/aarch64/QEMU_EFI.fd",
|
|
|
|
|
"/usr/share/edk2/aarch64/QEMU_EFI-silent.fd",
|
|
|
|
|
"/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
|
|
|
|
|
"/usr/share/edk2-armvirt/aarch64/QEMU_EFI.fd",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Gestionnaires de paquets, dans l'ordre de préférence :
|
|
|
|
|
# (clé TOOL_PACKAGES, binaire à détecter, commande d'installation, sudo, refresh)
|
|
|
|
|
PKG_MANAGERS: tuple[
|
|
|
|
|
tuple[str, str, list[str], bool, list[str] | None], ...
|
|
|
|
|
] = (
|
|
|
|
|
(
|
|
|
|
|
"apt",
|
|
|
|
|
"apt-get",
|
|
|
|
|
["apt-get", "install", "-y"],
|
|
|
|
|
True,
|
|
|
|
|
["apt-get", "update"],
|
|
|
|
|
),
|
|
|
|
|
("dnf", "dnf", ["dnf", "install", "-y"], True, None),
|
|
|
|
|
(
|
|
|
|
|
"pacman",
|
|
|
|
|
"pacman",
|
|
|
|
|
["pacman", "-S", "--needed", "--noconfirm"],
|
|
|
|
|
True,
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"zypper",
|
|
|
|
|
"zypper",
|
|
|
|
|
["zypper", "--non-interactive", "install"],
|
|
|
|
|
True,
|
|
|
|
|
None,
|
|
|
|
|
),
|
|
|
|
|
("brew", "brew", ["brew", "install"], False, None),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def detect_pkg_manager() -> (
|
|
|
|
|
tuple[str, list[str], bool, list[str] | None] | None
|
|
|
|
|
):
|
|
|
|
|
"""Retourne (clé, cmd d'install, use_sudo, cmd de refresh) ou None."""
|
|
|
|
|
for key, binary, install_cmd, use_sudo, refresh in PKG_MANAGERS:
|
|
|
|
|
if shutil.which(binary):
|
|
|
|
|
return key, install_cmd, use_sudo, refresh
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def missing_tools() -> list[str]:
|
|
|
|
|
"""Binaires requis absents du PATH (dont un outil de seed si aucun présent)."""
|
|
|
|
|
missing = [t for t in REQUIRED_TOOLS if shutil.which(t) is None]
|
|
|
|
|
if not any(shutil.which(t) for t in SEED_TOOLS):
|
|
|
|
|
missing.append(SEED_TOOLS[0]) # on installera cloud-localds
|
|
|
|
|
return missing
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prompt_yes_no(question: str, default: bool = True) -> bool:
|
|
|
|
|
"""Question oui/non. Lit /dev/tty pour rester visible même si stdout est
|
|
|
|
|
redirigé (cas d'un lancement depuis le menu todo)."""
|
|
|
|
|
suffix = " [O/n] " if default else " [o/N] "
|
|
|
|
|
prompt = question + suffix
|
|
|
|
|
try:
|
|
|
|
|
with open("/dev/tty", "r+") as tty:
|
|
|
|
|
tty.write(prompt)
|
|
|
|
|
tty.flush()
|
|
|
|
|
ans = tty.readline().strip().lower()
|
|
|
|
|
except OSError:
|
|
|
|
|
try:
|
|
|
|
|
ans = input(prompt).strip().lower()
|
|
|
|
|
except EOFError:
|
|
|
|
|
return default
|
|
|
|
|
if not ans:
|
|
|
|
|
return default
|
|
|
|
|
return ans in ("o", "oui", "y", "yes")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def daemon_missing() -> bool:
|
|
|
|
|
"""Vrai si le démon libvirt OU l'émulateur QEMU système est absent."""
|
|
|
|
|
libvirtd = shutil.which("libvirtd") or any(
|
|
|
|
|
os.path.exists(p) for p in ("/usr/sbin/libvirtd", "/usr/bin/libvirtd")
|
|
|
|
|
)
|
|
|
|
|
emulator = (
|
|
|
|
|
shutil.which("qemu-system-x86_64")
|
|
|
|
|
or shutil.which("qemu-system-x86")
|
|
|
|
|
or shutil.which("kvm")
|
|
|
|
|
)
|
|
|
|
|
return not (libvirtd and emulator)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def libvirt_ready(use_sudo: bool) -> bool:
|
|
|
|
|
"""Vrai si l'hyperviseur qemu:///system répond (démon libvirt démarré)."""
|
|
|
|
|
if shutil.which("virsh") is None:
|
|
|
|
|
return False
|
|
|
|
|
cmd = (["sudo"] if use_sudo else []) + [
|
|
|
|
|
"virsh",
|
|
|
|
|
"-c",
|
|
|
|
|
"qemu:///system",
|
|
|
|
|
"version",
|
|
|
|
|
]
|
|
|
|
|
try:
|
|
|
|
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
|
|
|
return res.returncode == 0
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_libvirt_service(runner: Runner) -> None:
|
|
|
|
|
"""S'assure que le démon libvirt tourne ; sinon le démarre puis vérifie."""
|
|
|
|
|
if libvirt_ready(runner.use_sudo):
|
|
|
|
|
return
|
|
|
|
|
if shutil.which("systemctl"):
|
|
|
|
|
print(
|
|
|
|
|
" Démarrage du démon libvirt"
|
|
|
|
|
" (systemctl enable --now libvirtd)…"
|
|
|
|
|
)
|
|
|
|
|
runner.run(
|
|
|
|
|
["systemctl", "enable", "--now", "libvirtd"],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
runner.run(
|
|
|
|
|
["systemctl", "start", "libvirtd.socket"],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
# Le socket peut mettre un instant à apparaître.
|
|
|
|
|
for _ in range(10):
|
|
|
|
|
if libvirt_ready(runner.use_sudo):
|
|
|
|
|
return
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
sys.exit(
|
|
|
|
|
"Erreur : impossible de se connecter à l'hyperviseur libvirt"
|
|
|
|
|
" (/var/run/libvirt/libvirt-sock).\n"
|
|
|
|
|
" Le démon libvirtd n'est pas démarré. Essayez :\n"
|
|
|
|
|
" sudo systemctl enable --now libvirtd\n"
|
|
|
|
|
" puis vérifiez : sudo virsh -c qemu:///system version"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def invoking_user() -> str:
|
|
|
|
|
"""Utilisateur réel derrière l'appel, même sous sudo."""
|
|
|
|
|
return os.environ.get("SUDO_USER") or getpass.getuser()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_libvirt_group(runner: Runner) -> bool:
|
|
|
|
|
"""Ajoute l'utilisateur au groupe libvirt. Renvoie True s'il l'était déjà.
|
|
|
|
|
|
|
|
|
|
SANS ce groupe, les clients libvirt d'un utilisateur non root retombent sur
|
|
|
|
|
« qemu:///session », où le réseau « default » N'EXISTE PAS : virt-install
|
|
|
|
|
échoue alors sur « --network network=default » alors que tous les paquets
|
|
|
|
|
sont pourtant installés. C'est le trou qui manquait au profil
|
|
|
|
|
« ERPLibre Déploiement » : il installait QEMU sans jamais rendre
|
|
|
|
|
qemu:///system accessible.
|
|
|
|
|
"""
|
|
|
|
|
user = invoking_user()
|
|
|
|
|
if user == "root":
|
|
|
|
|
return True
|
|
|
|
|
lst_group = []
|
|
|
|
|
for group in ("libvirt", "kvm"):
|
|
|
|
|
try:
|
|
|
|
|
grp.getgrnam(group)
|
|
|
|
|
except KeyError:
|
|
|
|
|
continue # groupe absent sur cette distro
|
|
|
|
|
try:
|
|
|
|
|
if user in grp.getgrnam(group).gr_mem:
|
|
|
|
|
continue
|
|
|
|
|
except KeyError:
|
|
|
|
|
pass
|
|
|
|
|
lst_group.append(group)
|
|
|
|
|
if not lst_group:
|
|
|
|
|
print(f" Utilisateur « {user} » déjà dans les groupes libvirt/kvm.")
|
|
|
|
|
return True
|
|
|
|
|
for group in lst_group:
|
|
|
|
|
print(f" Ajout de « {user} » au groupe « {group} »…")
|
|
|
|
|
runner.run(
|
|
|
|
|
["usermod", "-aG", group, user], privileged=True, check=False
|
|
|
|
|
)
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def kernel_modules_stale() -> str:
|
|
|
|
|
"""Renvoie un message si les modules du noyau EN COURS ont disparu.
|
|
|
|
|
|
|
|
|
|
Sur une distro roulante, « make install_os » met le noyau à jour et le
|
|
|
|
|
gestionnaire de paquets SUPPRIME /lib/modules/<noyau en cours>. Tant qu'on
|
|
|
|
|
n'a pas redémarré, « modprobe bridge » échoue et libvirt ne peut pas créer
|
|
|
|
|
virbr0 : « Unable to create bridge virbr0: Package not installed ». Aucun
|
|
|
|
|
paquet ne corrige ça, seul un redémarrage le fait — d'où ce diagnostic.
|
|
|
|
|
"""
|
|
|
|
|
if not shutil.which("uname"):
|
|
|
|
|
return ""
|
|
|
|
|
running = os.uname().release
|
|
|
|
|
path_module = f"/lib/modules/{running}"
|
|
|
|
|
if os.path.isdir(path_module):
|
|
|
|
|
return ""
|
|
|
|
|
return (
|
|
|
|
|
f"Le noyau en cours ({running}) n'a plus ses modules "
|
|
|
|
|
f"({path_module} absent) : il a été mis à jour depuis le démarrage.\n"
|
|
|
|
|
" libvirt ne pourra pas créer le pont virbr0 tant que la machine\n"
|
|
|
|
|
" n'aura pas REDÉMARRÉ. Redémarrez, puis relancez --setup-host."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_ssh_key(runner: Runner) -> None:
|
|
|
|
|
"""Garantit que l'utilisateur possède une clé publique SSH.
|
|
|
|
|
|
|
|
|
|
Sans clé, cloud-init ne peut en injecter aucune dans la VM créée : elle
|
|
|
|
|
démarre sans accès SSH et l'orchestrateur ne peut plus vérifier son état.
|
|
|
|
|
On en génère donc une (ed25519, sans passphrase) quand il n'y en a pas.
|
|
|
|
|
|
|
|
|
|
La clé doit appartenir à l'UTILISATEUR, pas à root : sous sudo, « ~ »
|
|
|
|
|
désigne /root et la clé y serait inutilisable.
|
|
|
|
|
"""
|
|
|
|
|
user = invoking_user()
|
|
|
|
|
home = os.path.expanduser(f"~{user}")
|
|
|
|
|
ssh_dir = os.path.join(home, ".ssh")
|
|
|
|
|
for name in ("id_ed25519.pub", "id_rsa.pub"):
|
|
|
|
|
path_pub = os.path.join(ssh_dir, name)
|
|
|
|
|
if os.path.exists(path_pub):
|
|
|
|
|
print(f" Clé SSH déjà présente : {path_pub}")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
path_key = os.path.join(ssh_dir, "id_ed25519")
|
|
|
|
|
print(f" Génération d'une clé SSH ed25519 pour « {user} »…")
|
|
|
|
|
# sudo -u : on exécute EN TANT QUE l'utilisateur, pour que la clé et le
|
|
|
|
|
# répertoire lui appartiennent même quand --setup-host tourne sous sudo.
|
|
|
|
|
prefix = (
|
|
|
|
|
["sudo", "-u", user] if os.geteuid() == 0 and user != "root" else []
|
|
|
|
|
)
|
|
|
|
|
runner.run(prefix + ["mkdir", "-p", "-m", "700", ssh_dir], check=False)
|
|
|
|
|
runner.run(
|
|
|
|
|
prefix
|
|
|
|
|
+ [
|
|
|
|
|
"ssh-keygen",
|
|
|
|
|
"-t",
|
|
|
|
|
"ed25519",
|
|
|
|
|
"-N",
|
|
|
|
|
"",
|
|
|
|
|
"-q",
|
|
|
|
|
"-f",
|
|
|
|
|
path_key,
|
|
|
|
|
"-C",
|
|
|
|
|
f"erplibre-deploy@{socket.gethostname()}",
|
|
|
|
|
],
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
if not runner.dry_run and os.path.exists(f"{path_key}.pub"):
|
|
|
|
|
print(f" Clé créée : {path_key}.pub")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def schedule_reboot(runner: Runner) -> None:
|
|
|
|
|
"""Programme un redémarrage DIFFÉRÉ et détaché de la session courante.
|
|
|
|
|
|
|
|
|
|
Un « systemctl reboot » immédiat tuerait le SSH de l'installation, et
|
|
|
|
|
l'orchestrateur compterait la VM en échec alors que tout s'est bien passé.
|
|
|
|
|
On laisse donc quelques secondes pour que la commande distante rende la
|
|
|
|
|
main proprement.
|
|
|
|
|
"""
|
|
|
|
|
if shutil.which("systemd-run"):
|
|
|
|
|
runner.run(
|
|
|
|
|
["systemd-run", "--on-active=5", "systemctl", "reboot"],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
runner.run(["shutdown", "-r", "+1"], privileged=True, check=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def setup_host(
|
|
|
|
|
runner: Runner,
|
|
|
|
|
assume_yes: bool,
|
|
|
|
|
no_install: bool,
|
|
|
|
|
reboot_if_needed: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Prépare l'hôte à faire tourner des VM : paquets, démon, groupe, réseau.
|
|
|
|
|
|
|
|
|
|
Point d'entrée unique du profil d'installation « ERPLibre Déploiement ».
|
|
|
|
|
Il réutilise les mêmes fonctions que le déploiement, donc les noms de
|
|
|
|
|
paquets restent définis à UN SEUL endroit (TOOL_PACKAGES /
|
|
|
|
|
DAEMON_PACKAGES) et restent valides pour apt, dnf, pacman, zypper et brew.
|
|
|
|
|
"""
|
|
|
|
|
print("\n== Préparation de l'hôte pour QEMU/libvirt ==")
|
|
|
|
|
# ensure_tools installe les clients ET, si le démon manque, les paquets de
|
|
|
|
|
# DAEMON_PACKAGES (qemu système + libvirt + firmware UEFI), puis démarre le
|
|
|
|
|
# service. ensure_emulator ne sert QU'À l'émulation croisée (arm64 sur x86)
|
|
|
|
|
# et n'a pas de clé pour l'architecture hôte.
|
|
|
|
|
ensure_tools(runner, assume_yes, no_install, force_daemon=True)
|
|
|
|
|
ensure_libvirt_service(runner)
|
|
|
|
|
already_in_group = ensure_libvirt_group(runner)
|
|
|
|
|
ensure_ssh_key(runner)
|
|
|
|
|
# Diagnostiqué AVANT le réseau : sans les modules du noyau en cours, le
|
|
|
|
|
# pont virbr0 est impossible et l'erreur de virsh est indéchiffrable.
|
|
|
|
|
stale = kernel_modules_stale()
|
|
|
|
|
if stale:
|
|
|
|
|
print(f"\n⚠ {stale}")
|
|
|
|
|
ensure_network("default", runner)
|
|
|
|
|
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
print("\n[dry-run] Rien n'a été modifié, vérification ignorée.")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print("\n== Vérification ==")
|
|
|
|
|
ok = libvirt_ready(runner.use_sudo)
|
|
|
|
|
print(f" hyperviseur qemu:///system : {'OK' if ok else 'INJOIGNABLE'}")
|
|
|
|
|
active, autostart = network_state("default", runner.use_sudo)
|
|
|
|
|
print(
|
|
|
|
|
f" réseau libvirt « default » : "
|
|
|
|
|
f"{'actif' if active else 'INACTIF'}"
|
|
|
|
|
f" / {'autostart' if autostart else 'PAS autostart'}"
|
|
|
|
|
)
|
|
|
|
|
if not active and stale:
|
|
|
|
|
# Le réseau est déjà « autostart » : après le redémarrage, libvirt le
|
|
|
|
|
# monte tout seul avec les modules du nouveau noyau. Un seul reboot
|
|
|
|
|
# suffit donc à rendre l'hôte utilisable, sans repasser par ici.
|
|
|
|
|
if reboot_if_needed:
|
|
|
|
|
print(
|
|
|
|
|
"\n↻ Redémarrage programmé (dans quelques secondes) : c'est la"
|
|
|
|
|
" SEULE façon de retrouver les modules du noyau.\n"
|
|
|
|
|
" Au retour, le réseau « default » démarrera seul"
|
|
|
|
|
" (autostart déjà actif)."
|
|
|
|
|
)
|
|
|
|
|
schedule_reboot(runner)
|
|
|
|
|
return
|
|
|
|
|
sys.exit(f"Erreur : l'hôte n'est pas prêt.\n {stale}")
|
|
|
|
|
|
|
|
|
|
if not (ok and active):
|
|
|
|
|
sys.exit(
|
|
|
|
|
"Erreur : l'hôte n'est pas prêt.\n"
|
|
|
|
|
+ (f" {stale}\n" if stale else "")
|
|
|
|
|
+ " Sinon vérifiez :\n"
|
|
|
|
|
" sudo systemctl enable --now libvirtd\n"
|
|
|
|
|
" sudo virsh -c qemu:///system net-start default"
|
|
|
|
|
)
|
|
|
|
|
if not already_in_group:
|
|
|
|
|
print(
|
|
|
|
|
f"\n⚠ « {invoking_user()} » vient d'être ajouté au groupe libvirt."
|
|
|
|
|
" Les groupes ne s'appliquent qu'aux NOUVELLES sessions :"
|
|
|
|
|
" reconnectez-vous (ou « newgrp libvirt »), sinon virt-install"
|
|
|
|
|
" continuera d'utiliser qemu:///session et le réseau « default »"
|
|
|
|
|
" restera introuvable."
|
|
|
|
|
)
|
|
|
|
|
print("\nHôte prêt.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_tools(
|
|
|
|
|
runner: Runner,
|
|
|
|
|
assume_yes: bool,
|
|
|
|
|
no_install: bool,
|
|
|
|
|
force_daemon: bool = False,
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Vérifie outils, démon libvirt et émulateur ; installe/démarre ce qui
|
|
|
|
|
manque, puis vérifie la connexion à l'hyperviseur.
|
|
|
|
|
|
|
|
|
|
`force_daemon` réinstalle DAEMON_PACKAGES même quand le démon répond déjà.
|
|
|
|
|
Vécu sur Arch : libvirt était présent (posé par un ancien one-liner qui ne
|
|
|
|
|
listait pas dnsmasq), donc daemon_missing() renvoyait False et dnsmasq
|
|
|
|
|
n'était jamais installé -> « Failed to start network default ». Les
|
|
|
|
|
gestionnaires de paquets ignorent ce qui est déjà là : c'est bon marché.
|
|
|
|
|
"""
|
|
|
|
|
missing = missing_tools()
|
|
|
|
|
need_daemon = daemon_missing() or force_daemon
|
|
|
|
|
|
|
|
|
|
# Tout est là et l'hyperviseur répond : rien à faire.
|
|
|
|
|
if not missing and not need_daemon and libvirt_ready(runner.use_sudo):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
pm = detect_pkg_manager()
|
|
|
|
|
if pm is None:
|
|
|
|
|
sys.exit(
|
|
|
|
|
" Gestionnaire de paquets non reconnu "
|
|
|
|
|
"(apt/dnf/pacman/zypper/brew).\n"
|
|
|
|
|
" Installez manuellement : " + ", ".join(missing)
|
|
|
|
|
)
|
|
|
|
|
pm_key, install_cmd, use_sudo, refresh = pm
|
|
|
|
|
|
|
|
|
|
pkg_map = TOOL_PACKAGES[pm_key]
|
|
|
|
|
packages = [pkg_map.get(t, t) for t in missing]
|
|
|
|
|
if need_daemon:
|
|
|
|
|
packages += DAEMON_PACKAGES.get(pm_key, [])
|
|
|
|
|
packages = list(dict.fromkeys(packages)) # dédoublonne, ordre gardé
|
|
|
|
|
|
|
|
|
|
if packages:
|
|
|
|
|
label = list(missing)
|
|
|
|
|
if need_daemon:
|
|
|
|
|
label.append("démon libvirt / émulateur QEMU")
|
|
|
|
|
print(" Composants manquants : " + ", ".join(label))
|
|
|
|
|
|
|
|
|
|
if no_install:
|
|
|
|
|
sys.exit(
|
|
|
|
|
" Installation automatique désactivée (--no-install-deps).\n"
|
|
|
|
|
" Installez manuellement : " + " ".join(packages)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
full_cmd = install_cmd + packages
|
|
|
|
|
printable = " ".join(full_cmd)
|
|
|
|
|
if use_sudo and runner.use_sudo:
|
|
|
|
|
printable = "sudo " + printable
|
|
|
|
|
|
|
|
|
|
print(f" Gestionnaire détecté : {pm_key}")
|
|
|
|
|
print(f" Commande d'installation : {printable}")
|
|
|
|
|
|
|
|
|
|
if not assume_yes and not prompt_yes_no(
|
|
|
|
|
" Installer ces dépendances maintenant ?"
|
|
|
|
|
):
|
|
|
|
|
sys.exit(
|
|
|
|
|
" Installation refusée.\n Commande manuelle : " + printable
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if refresh:
|
|
|
|
|
runner.run(refresh, privileged=use_sudo, check=False)
|
|
|
|
|
runner.run(full_cmd, privileged=use_sudo)
|
|
|
|
|
|
|
|
|
|
still = missing_tools()
|
|
|
|
|
# Repli : si seul l'outil de seed manque encore (le nom du paquet
|
|
|
|
|
# cloud-localds varie selon la distro), tente genisoimage.
|
|
|
|
|
if still == [SEED_TOOLS[0]]:
|
|
|
|
|
alt = pkg_map.get("genisoimage", "genisoimage")
|
|
|
|
|
print(f" Repli sur {alt} (autre outil de seed cloud-init)…")
|
|
|
|
|
runner.run(install_cmd + [alt], privileged=use_sudo, check=False)
|
|
|
|
|
still = missing_tools()
|
|
|
|
|
if still:
|
|
|
|
|
sys.exit(
|
|
|
|
|
" Outils toujours absents après installation : "
|
|
|
|
|
+ ", ".join(still)
|
|
|
|
|
+ f"\n Essayez manuellement : {printable}"
|
|
|
|
|
)
|
|
|
|
|
print(" Dépendances installées avec succès.")
|
|
|
|
|
|
|
|
|
|
# Démarre le démon (si nécessaire) et vérifie l'accès à l'hyperviseur.
|
|
|
|
|
ensure_libvirt_service(runner)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def emulator_ready(arch: str) -> bool:
|
|
|
|
|
"""Vrai si l'émulateur (et, pour arm64, un firmware UEFI AAVMF) est là."""
|
|
|
|
|
if shutil.which(EMULATOR_BINARY[arch]) is None:
|
|
|
|
|
return False
|
|
|
|
|
if arch == "arm64":
|
|
|
|
|
return any(os.path.exists(p) for p in AARCH64_FIRMWARE_PATHS)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_emulator(
|
|
|
|
|
arch: str, runner: Runner, assume_yes: bool, no_install: bool
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Vérifie l'émulateur QEMU système pour `arch` (et le firmware UEFI pour
|
|
|
|
|
arm64), requis quand on émule une architecture différente de l'hôte ;
|
|
|
|
|
l'installe au besoin via le gestionnaire de paquets."""
|
|
|
|
|
binary = EMULATOR_BINARY[arch]
|
|
|
|
|
if emulator_ready(arch):
|
|
|
|
|
return
|
|
|
|
|
pm = detect_pkg_manager()
|
|
|
|
|
if pm is None:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f" Émulateur {arch} introuvable ({binary}) et gestionnaire de "
|
|
|
|
|
"paquets non reconnu.\n Installez-le manuellement."
|
|
|
|
|
)
|
|
|
|
|
pm_key, install_cmd, use_sudo, refresh = pm
|
|
|
|
|
packages = EMULATOR_PACKAGES.get(arch, {}).get(pm_key, [])
|
|
|
|
|
if not packages:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f" Émulateur {arch} introuvable ({binary}) : installez le paquet "
|
|
|
|
|
"QEMU système correspondant de votre distribution "
|
|
|
|
|
"(+ firmware UEFI pour arm64)."
|
|
|
|
|
)
|
|
|
|
|
full_cmd = install_cmd + packages
|
|
|
|
|
printable = ("sudo " if use_sudo and runner.use_sudo else "") + " ".join(
|
|
|
|
|
full_cmd
|
|
|
|
|
)
|
|
|
|
|
print(f" Émulateur {arch} manquant ({binary}).")
|
|
|
|
|
print(f" Commande d'installation : {printable}")
|
|
|
|
|
if no_install:
|
|
|
|
|
sys.exit(
|
|
|
|
|
" Installation automatique désactivée (--no-install-deps).\n"
|
|
|
|
|
" Installez manuellement : " + printable
|
|
|
|
|
)
|
|
|
|
|
if not assume_yes and not prompt_yes_no(
|
|
|
|
|
f" Installer l'émulateur {arch} maintenant ?"
|
|
|
|
|
):
|
|
|
|
|
sys.exit(" Installation refusée.\n Commande manuelle : " + printable)
|
|
|
|
|
if refresh:
|
|
|
|
|
runner.run(refresh, privileged=use_sudo, check=False)
|
|
|
|
|
runner.run(full_cmd, privileged=use_sudo)
|
|
|
|
|
if not emulator_ready(arch):
|
|
|
|
|
sys.exit(
|
|
|
|
|
f" Émulateur {arch} ({binary}) ou firmware toujours absent après "
|
|
|
|
|
f"installation.\n Essayez manuellement : {printable}"
|
|
|
|
|
)
|
|
|
|
|
print(f" Émulateur {arch} installé avec succès.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Étapes
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# Délai (s) par opération réseau : au-delà, on abandonne le miroir courant.
|
|
|
|
|
# Sans lui, un miroir injoignable ferait pendre le téléchargement à l'infini.
|
|
|
|
|
DOWNLOAD_TIMEOUT = 30
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _download_one(url: str, tmp: Path, timeout: int) -> None:
|
|
|
|
|
"""Télécharge url -> tmp en streaming, avec timeout et barre de %.
|
|
|
|
|
Lève une exception en cas d'échec réseau (miroir suivant à essayer)."""
|
|
|
|
|
is_tty = sys.stdout.isatty()
|
|
|
|
|
last_pct = -1
|
|
|
|
|
req = urllib.request.Request(
|
|
|
|
|
url, headers={"User-Agent": "erplibre-qemu-deploy"}
|
|
|
|
|
)
|
|
|
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
|
|
|
total = int(resp.headers.get("Content-Length", 0) or 0)
|
|
|
|
|
done = 0
|
|
|
|
|
with open(tmp, "wb") as fh:
|
|
|
|
|
while True:
|
|
|
|
|
chunk = resp.read(1 << 16)
|
|
|
|
|
if not chunk:
|
|
|
|
|
break
|
|
|
|
|
fh.write(chunk)
|
|
|
|
|
done += len(chunk)
|
|
|
|
|
if total <= 0:
|
|
|
|
|
continue
|
|
|
|
|
pct = min(100, done * 100 // total)
|
|
|
|
|
if pct == last_pct:
|
|
|
|
|
continue
|
|
|
|
|
last_pct = pct
|
|
|
|
|
# TTY : une ligne réécrite (\r) ; sinon (capturé par le menu
|
|
|
|
|
# todo) au plus 101 lignes, jamais de flot infini.
|
|
|
|
|
if is_tty:
|
|
|
|
|
print(f"\r {pct:3d}%", end="", flush=True)
|
|
|
|
|
else:
|
|
|
|
|
print(f" {pct:3d}%", flush=True)
|
|
|
|
|
if is_tty:
|
|
|
|
|
print()
|
|
|
|
|
# Vérifie la COMPLÉTUDE : si le serveur a annoncé une taille et qu'on a
|
|
|
|
|
# reçu moins (connexion coupée), le .part est TRONQUÉ. Sans ce contrôle,
|
|
|
|
|
# il était validé comme « complet » -> qcow2 valide mais VIDE (juste
|
|
|
|
|
# l'en-tête) -> VM qui ne boote pas, et cache empoisonné réutilisé ensuite.
|
|
|
|
|
if total > 0 and done < total:
|
|
|
|
|
raise OSError(
|
|
|
|
|
f"téléchargement incomplet : {done}/{total} octets reçus "
|
|
|
|
|
"(connexion interrompue)"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def download_image(
|
|
|
|
|
urls, dest: Path, dry_run: bool, timeout: int = DOWNLOAD_TIMEOUT
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Télécharge l'image (essaie chaque miroir dans l'ordre) si absente du
|
|
|
|
|
cache. Échoue proprement — jamais de blocage infini — grâce au timeout."""
|
|
|
|
|
if isinstance(urls, str):
|
|
|
|
|
urls = [urls]
|
|
|
|
|
if dest.exists() and dest.stat().st_size > 0:
|
|
|
|
|
size_mb = dest.stat().st_size / 1024 / 1024
|
|
|
|
|
print(
|
|
|
|
|
f" Image déjà présente ({size_mb:.0f} Mo), téléchargement"
|
|
|
|
|
f" ignoré : {dest}",
|
|
|
|
|
flush=True,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
if dry_run:
|
|
|
|
|
print(f" [dry-run] téléchargement {urls[0]} -> {dest}", flush=True)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
except PermissionError:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"\nPermission refusée pour écrire dans {dest.parent}.\n"
|
|
|
|
|
" Relancez avec sudo, ou choisissez --image-dir vers un dossier"
|
|
|
|
|
" accessible en écriture."
|
|
|
|
|
)
|
|
|
|
|
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
|
|
|
errors = []
|
|
|
|
|
had_404 = False
|
|
|
|
|
for i, url in enumerate(urls, 1):
|
|
|
|
|
tag = "" if len(urls) == 1 else f" (miroir {i}/{len(urls)})"
|
|
|
|
|
print(f" Téléchargement{tag} : {url}", flush=True)
|
|
|
|
|
try:
|
|
|
|
|
_download_one(url, tmp, timeout)
|
|
|
|
|
tmp.replace(dest)
|
|
|
|
|
return
|
|
|
|
|
except urllib.error.HTTPError as exc: # image absente sur ce miroir
|
|
|
|
|
tmp.unlink(missing_ok=True)
|
|
|
|
|
had_404 = had_404 or exc.code == 404
|
|
|
|
|
print(f"\n Échec : HTTP {exc.code}", flush=True)
|
|
|
|
|
errors.append(f"{url} -> HTTP {exc.code}")
|
|
|
|
|
except Exception as exc: # réseau/timeout : miroir suivant
|
|
|
|
|
tmp.unlink(missing_ok=True)
|
|
|
|
|
print(f"\n Échec : {exc}", flush=True)
|
|
|
|
|
errors.append(f"{url} -> {exc}")
|
|
|
|
|
hint = (
|
|
|
|
|
"\n Image introuvable (404) : cette version est probablement EOL et"
|
|
|
|
|
" a été retirée du miroir. Choisissez une version LTS encore"
|
|
|
|
|
" supportée (voir --list-images)."
|
|
|
|
|
if had_404
|
|
|
|
|
else "\n Vérifiez la connectivité (IPv6 ?), réessayez plus tard, ou"
|
|
|
|
|
" fournissez un chemin d'image local en argument positionnel."
|
|
|
|
|
)
|
|
|
|
|
# Chemin visé + commande prête à copier pour reprendre le téléchargement
|
|
|
|
|
# manuellement (reprise si un .part existe), puis relancer le déploiement
|
|
|
|
|
# (l'image complète en cache sera réutilisée). curl -C - reprend, -f
|
|
|
|
|
# échoue proprement sur une erreur HTTP.
|
|
|
|
|
resume = (
|
|
|
|
|
f"\n Destination : {dest}"
|
|
|
|
|
f"\n Reprendre le téléchargement manuellement :"
|
|
|
|
|
f"\n sudo curl -fL -C - -o {dest} \\\n {urls[0]}"
|
|
|
|
|
"\n puis relancez le déploiement (l'image en cache sera réutilisée)."
|
|
|
|
|
)
|
|
|
|
|
sys.exit(
|
|
|
|
|
"\nÉchec du téléchargement depuis tous les miroirs :\n "
|
|
|
|
|
+ "\n ".join(errors)
|
|
|
|
|
+ hint
|
|
|
|
|
+ resume
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_sha256(url: str, image: Path, dry_run: bool) -> None:
|
|
|
|
|
"""Vérifie l'empreinte via le SHA256SUMS publié dans le même répertoire."""
|
|
|
|
|
if dry_run:
|
|
|
|
|
print(" [dry-run] vérification SHA256 ignorée")
|
|
|
|
|
return
|
|
|
|
|
sums_url = url.rsplit("/", 1)[0] + "/SHA256SUMS"
|
|
|
|
|
filename = url.rsplit("/", 1)[1]
|
|
|
|
|
print(f" Vérification SHA256 via {sums_url}")
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen( # noqa: S310
|
|
|
|
|
sums_url, timeout=DOWNLOAD_TIMEOUT
|
|
|
|
|
) as resp:
|
|
|
|
|
sums = resp.read().decode()
|
|
|
|
|
except Exception as exc: # pragma: no cover
|
|
|
|
|
sys.exit(f"Impossible de récupérer SHA256SUMS : {exc}")
|
|
|
|
|
|
|
|
|
|
expected = next(
|
|
|
|
|
(
|
|
|
|
|
line.split()[0]
|
|
|
|
|
for line in sums.splitlines()
|
|
|
|
|
if line.strip().endswith(filename)
|
|
|
|
|
),
|
|
|
|
|
None,
|
|
|
|
|
)
|
|
|
|
|
if expected is None:
|
|
|
|
|
sys.exit(f"Empreinte introuvable pour {filename} dans SHA256SUMS")
|
|
|
|
|
|
|
|
|
|
h = hashlib.sha256()
|
|
|
|
|
with image.open("rb") as fh:
|
|
|
|
|
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
|
|
|
h.update(chunk)
|
|
|
|
|
if h.hexdigest() != expected:
|
|
|
|
|
image.unlink(
|
|
|
|
|
missing_ok=True
|
|
|
|
|
) # évite la réutilisation du cache corrompu
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"SHA256 NON conforme ! Image supprimée : {image}\n"
|
|
|
|
|
f" attendu : {expected}\n obtenu : {h.hexdigest()}"
|
|
|
|
|
)
|
|
|
|
|
print(" SHA256 conforme.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def hash_password(plain: str) -> str:
|
|
|
|
|
"""Hash SHA-512 crypt ($6$...) via crypt (< 3.13) ou openssl en repli."""
|
|
|
|
|
try:
|
|
|
|
|
with warnings.catch_warnings():
|
|
|
|
|
# crypt est déprécié (retiré en 3.13) : on masque l'avertissement,
|
|
|
|
|
# le repli openssl couvre les versions récentes de Python.
|
|
|
|
|
warnings.simplefilter("ignore", DeprecationWarning)
|
|
|
|
|
import crypt
|
|
|
|
|
|
|
|
|
|
return crypt.crypt(plain, crypt.mksalt(crypt.METHOD_SHA512))
|
|
|
|
|
except (ImportError, AttributeError):
|
|
|
|
|
need_tool("openssl")
|
|
|
|
|
out = subprocess.run(
|
|
|
|
|
["openssl", "passwd", "-6", "-stdin"],
|
|
|
|
|
input=plain + "\n",
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
return out.stdout.strip()
|
|
|
|
|
|
|
|
|
|
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
# Miroirs apt, du plus rapide au dernier recours. cloud-init prend le PREMIER
|
|
|
|
|
# joignable de la liste « search » : l'ordre est donc la priorité.
|
|
|
|
|
#
|
|
|
|
|
# Mesuré depuis Montréal sur l'index main/s390x (1,6 Mo) :
|
|
|
|
|
# ports.ubuntu.com 1,61 s 1,0 Mo/s
|
|
|
|
|
# mirror.csclub.uwaterloo.ca 0,32 s 5,2 Mo/s
|
|
|
|
|
# mirror.us.leaseweb.net 0,88 s 1,9 Mo/s
|
|
|
|
|
#
|
|
|
|
|
# Le chemin diffère selon l'architecture : les arches « ports » (s390x, arm64,
|
|
|
|
|
# ppc64el, riscv64) ne sont PAS sur archive.ubuntu.com, et amd64 n'est pas sur
|
|
|
|
|
# ports.ubuntu.com. Une seule liste servirait donc la moitié des cas en 404.
|
|
|
|
|
APT_MIRRORS_PORTS = [
|
|
|
|
|
"http://mirror.csclub.uwaterloo.ca/ubuntu-ports",
|
|
|
|
|
"http://mirror.us.leaseweb.net/ubuntu-ports",
|
|
|
|
|
"http://ports.ubuntu.com/ubuntu-ports",
|
|
|
|
|
]
|
|
|
|
|
APT_MIRRORS_MAIN = [
|
|
|
|
|
"http://mirror.csclub.uwaterloo.ca/ubuntu",
|
|
|
|
|
"http://mirror.us.leaseweb.net/ubuntu",
|
|
|
|
|
"http://archive.ubuntu.com/ubuntu",
|
|
|
|
|
]
|
|
|
|
|
PORTS_ARCHES = ("s390x", "arm64", "aarch64", "ppc64el", "riscv64")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apt_mirror_lines(arch: str, override: str | None = None) -> list[str]:
|
|
|
|
|
"""Bloc « apt: » du cloud-config, ou [] si rien à écrire.
|
|
|
|
|
|
|
|
|
|
Ubuntu seulement : Debian, Fedora et Arch ont leurs propres dépôts, et
|
|
|
|
|
« search » y écrirait des URI qui n'existent pas.
|
|
|
|
|
"""
|
|
|
|
|
mirrors = (
|
|
|
|
|
[override]
|
|
|
|
|
if override
|
|
|
|
|
else (APT_MIRRORS_PORTS if arch in PORTS_ARCHES else APT_MIRRORS_MAIN)
|
|
|
|
|
)
|
|
|
|
|
lines = ["apt:", " primary:", " - arches: [default]", " search:"]
|
|
|
|
|
lines += [f" - {m}" for m in mirrors]
|
|
|
|
|
# La sécurité suit le même dépôt pour les arches ports ; sur amd64 elle a
|
|
|
|
|
# son propre hôte, que les miroirs répliquent sous le même chemin.
|
|
|
|
|
lines += [" security:", " - arches: [default]", " search:"]
|
|
|
|
|
lines += [f" - {m}" for m in mirrors]
|
|
|
|
|
return lines
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def kvm_available() -> bool:
|
|
|
|
|
"""L'accélération matérielle est-elle réellement utilisable ici ?
|
|
|
|
|
|
|
|
|
|
« Même architecture que l'hôte » ne suffit PAS à conclure à KVM : dans une
|
|
|
|
|
VM sans virtualisation imbriquée, libvirt bascule SILENCIEUSEMENT en TCG.
|
|
|
|
|
Mesuré sur erplibre01, lui-même invité KVM : une VM s390x sur hôte s390x
|
|
|
|
|
est sortie en « <domain type='qemu'> », soit de l'émulation intégrale — et
|
|
|
|
|
un démarrage de 7 min 30 au lieu de quelques dizaines de secondes, sans
|
|
|
|
|
que rien ne le signale.
|
|
|
|
|
|
|
|
|
|
/dev/kvm est le test que fait QEMU lui-même. Mais l'ACCÈS n'est concluant
|
|
|
|
|
que si on est root : libvirt, lui, tourne en root et se moque de notre
|
|
|
|
|
appartenance au groupe kvm. Tester nos propres droits en non-root ferait
|
|
|
|
|
crier « pas de KVM » à un utilisateur simplement hors du groupe.
|
|
|
|
|
"""
|
|
|
|
|
if not os.path.exists("/dev/kvm"):
|
|
|
|
|
return False
|
|
|
|
|
if os.geteuid() == 0:
|
|
|
|
|
return os.access("/dev/kvm", os.R_OK | os.W_OK)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def nested_module() -> str:
|
|
|
|
|
"""Module noyau portant le paramètre « nested » sur CET hôte.
|
|
|
|
|
|
|
|
|
|
Le nom change selon l'architecture, et se tromper de module fait lire un
|
|
|
|
|
« 0 » rassurant sur un fichier qui ne commande rien : s390x et arm64
|
|
|
|
|
l'exposent sur « kvm », x86 sur « kvm_intel » ou « kvm_amd » selon le
|
|
|
|
|
fabricant — et /sys/module/kvm/parameters/nested n'y existe même pas.
|
|
|
|
|
"""
|
|
|
|
|
arch = host_arch()
|
|
|
|
|
if arch != "amd64":
|
|
|
|
|
return "kvm"
|
|
|
|
|
try:
|
|
|
|
|
info = Path("/proc/cpuinfo").read_text(errors="replace")
|
|
|
|
|
except OSError:
|
|
|
|
|
return "kvm_intel"
|
|
|
|
|
return "kvm_amd" if " svm" in info else "kvm_intel"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def host_timezone() -> str:
|
|
|
|
|
"""Fuseau de l'hôte, au format zoneinfo (« America/Montreal »).
|
|
|
|
|
|
|
|
|
|
Défaut des VM déployées : une VM qui hérite du fuseau de la machine qui la
|
|
|
|
|
crée horodate ses journaux, ses commits et ses bases comme son opérateur.
|
|
|
|
|
Sans cela elle démarre en UTC, ce qui ne se voit qu'après coup — des commits
|
|
|
|
|
à +0000 alors que tout le reste du dépôt est en -0400.
|
|
|
|
|
|
|
|
|
|
Trois sources, de la plus fiable à la plus rustique ; « UTC » en dernier
|
|
|
|
|
recours plutôt qu'une exception, car un fuseau indéterminable ne doit pas
|
|
|
|
|
empêcher un déploiement.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
out = subprocess.run(
|
|
|
|
|
["timedatectl", "show", "-p", "Timezone", "--value"],
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=5,
|
|
|
|
|
).stdout.strip()
|
|
|
|
|
if out:
|
|
|
|
|
return out
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
tz = Path("/etc/timezone").read_text(encoding="utf-8").strip()
|
|
|
|
|
if tz:
|
|
|
|
|
return tz
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
try:
|
|
|
|
|
# /etc/localtime est un lien vers /usr/share/zoneinfo/<Zone>
|
|
|
|
|
target = Path("/etc/localtime").resolve()
|
|
|
|
|
parts = target.parts
|
|
|
|
|
if "zoneinfo" in parts:
|
|
|
|
|
return "/".join(parts[parts.index("zoneinfo") + 1 :])
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
return "UTC"
|
|
|
|
|
|
|
|
|
|
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
def user_groups(distro: str) -> str:
|
|
|
|
|
"""Groupes secondaires du compte créé par cloud-init.
|
|
|
|
|
|
|
|
|
|
Le nom du groupe d'administration change d'une famille à l'autre, et un
|
|
|
|
|
nom INCONNU fait échouer « useradd -G » : l'utilisateur n'est alors pas
|
|
|
|
|
créé du tout, donc ni mot de passe ni clé SSH, et la VM démarre
|
|
|
|
|
inaccessible. Le privilège vient de toute façon de la directive « sudo: »
|
|
|
|
|
du cloud-config, pas du groupe — celui-ci n'est qu'une commodité.
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
- Debian, Ubuntu : « sudo ».
|
|
|
|
|
- Famille RHEL (AlmaLinux, Rocky, CentOS, Fedora), Arch : « wheel ».
|
|
|
|
|
- openSUSE : aucun. Son cloud-init par défaut n'en met pas, et rien ne
|
|
|
|
|
garantit « wheel » sur une image Minimal-VM — dans le doute on s'abstient
|
|
|
|
|
plutôt que de risquer un compte non créé."""
|
|
|
|
|
if distro in ("ubuntu", "debian"):
|
|
|
|
|
return "users, sudo"
|
|
|
|
|
if distro == "opensuse":
|
|
|
|
|
return "users"
|
|
|
|
|
return "users, wheel"
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
|
|
|
|
|
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
def build_cloud_config(
|
|
|
|
|
args: argparse.Namespace, pw_hash: str | None, ssh_keys: list[str]
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Construit le contenu #cloud-config (user-data)."""
|
|
|
|
|
lines: list[str] = ["#cloud-config", f"hostname: {args.hostname}"]
|
|
|
|
|
|
|
|
|
|
user_block = [
|
|
|
|
|
"users:",
|
|
|
|
|
f" - name: {args.user}",
|
|
|
|
|
" sudo: ALL=(ALL) NOPASSWD:ALL",
|
[UPD] support: drop Ubuntu 20.04/22.04, add AlmaLinux and Rocky
Ubuntu 20.04 and 22.04 leave EVERY architecture, not just s390x. pikepdf
needs qpdf 12.2, whose build requires C++20, while focal ships GCC 9 and
publishes no g++-10 for s390x at all. Python 3.8, node 10, cargo 0.67 and
OpenSSL 1.1.1 each had a workaround; the pile of them did not. 18.04
follows, already off the lists. The refusal lands before any apt, this
script also serving existing machines.
AlmaLinux 9 and 10, Rocky 9 and 10 join the catalog on all four
architectures: the twelve "latest" URLs were opened, with no index to
parse unlike Fedora. They would have booted unreachable though -- the
cloud-config forced "groups: users, sudo", but the RHEL family has no
sudo group, only wheel, and an unknown group makes useradd fail, hence no
password and no key. The very trap already known for Debian, repeated
elsewhere. Host side, EPEL and CRB are enabled: without them most -devel
packages are missing, silently.
The server / graphical choice gains Cinnamon, the Linux Mint desktop,
from the distribution's own repositories. Mint's repository is set aside:
plain HTTP, and i386/amd64 only, which would rule out arm64 and s390x.
Along the way, dnf now installs an ENVIRONMENT rather than a group --
"gnome-desktop" brings gdm and gnome-shell but not base-x, hence no X
server.
--- FR ---
Ubuntu 20.04 et 22.04 partent de TOUTES les architectures, pas seulement
de s390x. pikepdf réclame qpdf 12.2, dont la compilation exige C++20,
quand focal livre GCC 9 et ne publie même pas de g++-10 pour s390x.
Python 3.8, node 10, cargo 0.67 et OpenSSL 1.1.1 avaient chacun leur
contournement ; leur accumulation, non. 18.04 suit, déjà hors des
listes. Le refus tombe avant tout apt, ce script servant aussi les
machines existantes.
AlmaLinux 9 et 10, Rocky 9 et 10 entrent au catalogue, sur les quatre
architectures : les douze URL « latest » ont été ouvertes, aucun index à
analyser contrairement à Fedora. Elles auraient pourtant démarré
inaccessibles — le cloud-config imposait « groups: users, sudo », or la
famille RHEL n'a pas de groupe sudo mais wheel, et un groupe inconnu fait
échouer useradd, donc ni mot de passe ni clé. C'est le piège déjà connu
pour Debian, reproduit ailleurs. Côté hôte, EPEL et CRB sont activés :
sans eux la plupart des -devel manquent, en silence.
Le choix serveur / graphique gagne Cinnamon, le bureau de Linux Mint,
depuis les dépôts de la distribution. Le dépôt de Mint lui-même est
écarté : il est en HTTP nu et ne publie que i386 et amd64, ce qui
exclurait arm64 et s390x. Au passage, dnf installe désormais un
ENVIRONNEMENT et non un groupe — « gnome-desktop » apporte gdm et
gnome-shell mais pas base-x, donc pas de serveur X.
Assisted-by: Claude Opus 5
2026-08-11 18:46:11 -04:00
|
|
|
# Le groupe d'administration N'A PAS le même nom partout, et un nom
|
|
|
|
|
# inconnu fait échouer « useradd -G » : l'utilisateur n'est alors jamais
|
|
|
|
|
# créé, donc ni mot de passe ni clé SSH — la VM démarre et reste
|
|
|
|
|
# inaccessible. C'était déjà la cause du « Debian ne marche pas »
|
|
|
|
|
# (« admin » n'existe pas sur Debian) ; la famille RHEL et Arch ont le
|
|
|
|
|
# même écart, elles n'ont pas de groupe « sudo » mais « wheel ».
|
|
|
|
|
# Le privilège lui-même vient de la ligne « sudo: » ci-dessus, pas du
|
|
|
|
|
# groupe : celui-ci n'est qu'une commodité.
|
[ADD] install: openSUSE support, mirrors and packages
Tumbleweed is the only catalog entry whose qpdf already clears the pikepdf
threshold: 12.3.2 against 12.2 required, so the half-hour qpdf build under
s390x emulation never runs there. It is also the only family foreign to
both RHEL and Debian, and it brings zypper, absent from the repository:
wired into the install_dev.sh dispatch, a dependency script, the remote
bootstrap and the desktop block.
Four SUSE traps, all met on real machines. Global options precede the
subcommand, and misplaced ones abort the call; without
"--auto-agree-with-licenses" zypper waits for an answer nobody gives in a
detached install. A compat package that PROVIDES another under a different
name makes zypper raise a conflict and drop the whole batch, so only what
nothing already provides is requested. pkg-config no longer exists as an
RPM, only as a capability. And a single mirror, unreachable one day, sent
everyone back to Europe -- three are probed in order now.
CentOS Stream is dropped again: it served as a canary but 10 duplicates
what Alma and Rocky already cover.
--- FR ---
Tumbleweed est la seule entrée du catalogue dont qpdf franchit déjà le
seuil de pikepdf : 12.3.2 contre 12.2 exigé, si bien que la demi-heure de
compilation de qpdf sous émulation s390x ne s'y déclenche jamais. C'est
aussi la seule famille étrangère à RHEL comme à Debian, et elle apporte
zypper, absent du dépôt : câblé dans l'aiguillage d'install_dev.sh, un
script de dépendances, l'amorçage distant et le bloc bureau.
Quatre pièges SUSE, tous rencontrés sur machine réelle. Les options
globales précèdent la sous-commande, mal placées elles arrêtent l'appel ;
sans « --auto-agree-with-licenses » zypper attend une réponse que personne
ne donne dans une installation détachée. Un paquet compat qui FOURNIT un
autre sous un nom différent fait lever un conflit à zypper, qui abandonne
le lot entier : on ne demande donc que ce que rien ne fournit déjà.
pkg-config n'existe plus comme RPM, seulement comme capacité. Et un miroir
unique, injoignable un jour, renvoyait tout le monde en Europe — trois
sont désormais sondés dans l'ordre.
CentOS Stream repart : il servait de canari, mais 10 double ce qu'Alma et
Rocky couvrent déjà.
Assisted-by: Claude Opus 5
2026-08-11 20:38:12 -04:00
|
|
|
f" groups: {user_groups(args.distro)}",
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
" shell: /bin/bash",
|
|
|
|
|
" lock_passwd: false" if pw_hash else " lock_passwd: true",
|
|
|
|
|
]
|
|
|
|
|
if pw_hash:
|
|
|
|
|
user_block.append(f' passwd: "{pw_hash}"')
|
|
|
|
|
if ssh_keys:
|
|
|
|
|
user_block.append(" ssh_authorized_keys:")
|
|
|
|
|
user_block += [f" - {k}" for k in ssh_keys]
|
|
|
|
|
lines += user_block
|
|
|
|
|
|
|
|
|
|
lines.append(f"ssh_pwauth: {'true' if pw_hash else 'false'}")
|
|
|
|
|
lines.append(f"locale: {args.locale}")
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
lines.append(f"timezone: {args.timezone}")
|
|
|
|
|
if getattr(args, "distro", "ubuntu") == "ubuntu":
|
|
|
|
|
lines += apt_mirror_lines(
|
|
|
|
|
getattr(args, "arch", "amd64"), getattr(args, "apt_mirror", None)
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
lines += [
|
|
|
|
|
"keyboard:",
|
|
|
|
|
f" layout: {args.keyboard_layout}",
|
|
|
|
|
f" variant: {args.keyboard_variant}",
|
|
|
|
|
]
|
|
|
|
|
# apt update/upgrade désactivés par défaut : sur un réseau lent/instable
|
|
|
|
|
# ils font pendre cloud-init au 1er boot (et retardent la dispo SSH). SSH
|
|
|
|
|
# est déjà présent dans les images cloud ; on l'active via runcmd sans apt.
|
|
|
|
|
# --apt-update réactive apt update (+ upgrade, sauf --no-upgrade).
|
|
|
|
|
do_update = args.apt_update
|
|
|
|
|
do_upgrade = args.apt_update and not args.no_upgrade
|
|
|
|
|
lines.append(f"package_update: {'true' if do_update else 'false'}")
|
|
|
|
|
lines.append(f"package_upgrade: {'true' if do_upgrade else 'false'}")
|
|
|
|
|
|
|
|
|
|
# On n'installe AUCUN paquet via cloud-init : cela exige le réseau au 1er
|
|
|
|
|
# boot et peut bloquer longtemps (dnf/apt/pacman lents sur réseau lent) —
|
|
|
|
|
# cloud-init reste alors « running » et tout ce qui suit attend. sshd est
|
|
|
|
|
# DÉJÀ présent dans toutes les images cloud (Ubuntu/Debian/Fedora/Arch) ;
|
|
|
|
|
# on l'active seulement (runcmd). Les outils nécessaires (curl/git/make…)
|
|
|
|
|
# sont installés — avec dépôts optimisés — par le bootstrap d'installation.
|
|
|
|
|
packages = list(dict.fromkeys(args.package)) # seulement --package
|
|
|
|
|
if packages:
|
|
|
|
|
lines.append("packages:")
|
|
|
|
|
lines += [f" - {p}" for p in packages]
|
|
|
|
|
# Active et démarre SSH quel que soit le nom du service (ssh sur
|
|
|
|
|
# Debian/Ubuntu, sshd sur Fedora/Arch) — sans quoi la VM peut booter
|
|
|
|
|
# sans SSH accessible.
|
|
|
|
|
lines += [
|
|
|
|
|
"runcmd:",
|
|
|
|
|
" - systemctl enable --now ssh 2>/dev/null"
|
|
|
|
|
" || systemctl enable --now sshd 2>/dev/null || true",
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
# Getty sur la console qui EXISTE VRAIMENT.
|
|
|
|
|
#
|
|
|
|
|
# Sur s390x, l'image attend /dev/ttysclp0 (généré depuis la ligne de
|
|
|
|
|
# commande noyau) alors que le périphérique réellement présent porte un
|
|
|
|
|
# autre nom : « Timed out waiting for device dev-ttysclp0.device », puis
|
|
|
|
|
# « Dependency failed for serial-getty@ttysclp0 ». La console affiche
|
|
|
|
|
# donc tout le démarrage mais n'offre AUCUNE invite de connexion — elle
|
|
|
|
|
# est en lecture seule par accident, et c'est justement le seul recours
|
|
|
|
|
# quand SSH ne répond pas. On active le getty sur le premier
|
|
|
|
|
# périphérique console présent. Ailleurs (x86/arm64) ttyS0 existe et son
|
|
|
|
|
# getty tourne déjà : « enable --now » n'y change rien.
|
|
|
|
|
" - for d in ttysclp0 sclp_line0 hvc0 ttyS0; do"
|
|
|
|
|
" test -c /dev/$d && systemctl enable --now serial-getty@$d.service"
|
|
|
|
|
" 2>/dev/null && break; done || true",
|
|
|
|
|
# qemu-guest-agent : installé APRÈS sshd, et surtout HORS de cloud-init.
|
|
|
|
|
#
|
|
|
|
|
# Mesuré sur Ubuntu 26.04 s390x : « apt-get install qemu-guest-agent »
|
|
|
|
|
# tire liburing2, ubuntu-helper-virt-hwe et ubuntu-virt depuis
|
|
|
|
|
# ports.ubuntu.com, et cloud-final tourne 9 min 47. Or le suivi
|
|
|
|
|
# d'installation attend « cloud-init status: done » : dix minutes
|
|
|
|
|
# d'attente pour un paquet accessoire, avant même de commencer le
|
|
|
|
|
# travail utile.
|
|
|
|
|
#
|
|
|
|
|
# systemd-run --no-block rend la main tout de suite : cloud-init termine
|
|
|
|
|
# en quelques secondes et l'agent apparaît quand il apparaît. On saute
|
|
|
|
|
# aussi l'installation quand qemu-ga est déjà là, ce qui est le cas de
|
|
|
|
|
# beaucoup d'images. Repli en ligne si systemd-run manque.
|
|
|
|
|
" - command -v qemu-ga >/dev/null 2>&1 ||"
|
|
|
|
|
" systemd-run --no-block --unit=erplibre-qga --collect"
|
|
|
|
|
" /bin/sh -c 'command -v apt-get >/dev/null && { apt-get update -qq"
|
|
|
|
|
" || true; apt-get install -y qemu-guest-agent; }"
|
|
|
|
|
" || command -v dnf >/dev/null && dnf install -y qemu-guest-agent"
|
|
|
|
|
" || command -v pacman >/dev/null && pacman -Sy --noconfirm"
|
|
|
|
|
" qemu-guest-agent' 2>/dev/null"
|
|
|
|
|
" || (command -v apt-get >/dev/null && (timeout 120 apt-get update -qq"
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
" || true; timeout 300 apt-get install -y qemu-guest-agent)) ||"
|
|
|
|
|
" (command -v dnf >/dev/null && timeout 300 dnf install -y"
|
|
|
|
|
" qemu-guest-agent) || (command -v pacman >/dev/null && timeout 300"
|
|
|
|
|
" pacman -Sy --noconfirm qemu-guest-agent) || true",
|
|
|
|
|
# Autorise guest-exec (bloqué par défaut sur certaines distros).
|
|
|
|
|
# On VIDE la liste de blocage (block-rpcs/blacklist) plutôt que
|
|
|
|
|
# d'utiliser allow-rpcs (liste BLANCHE : casserait les autres RPC).
|
|
|
|
|
# Les deux clés couvrent les versions anciennes (blacklist) et
|
|
|
|
|
# récentes (block-rpcs) de qemu-ga.
|
|
|
|
|
" - mkdir -p /etc/qemu && printf '[general]\\nblock-rpcs=\\n"
|
|
|
|
|
"blacklist=\\n' > /etc/qemu/qemu-ga.conf || true",
|
|
|
|
|
# Fedora/RHEL bloquent guest-exec via /etc/sysconfig/qemu-ga.
|
|
|
|
|
# « test -f » et NON « [ -f … ] » : en YAML, « - [ » démarre une
|
|
|
|
|
# séquence en flux -> user-data invalide -> cloud-init rejeté (aucun
|
|
|
|
|
# utilisateur créé, VM figée, login console impossible).
|
|
|
|
|
" - test -f /etc/sysconfig/qemu-ga && sed -i"
|
|
|
|
|
" 's/^BLACKLIST_RPC=.*/BLACKLIST_RPC=/'"
|
|
|
|
|
" /etc/sysconfig/qemu-ga || true",
|
|
|
|
|
" - systemctl enable qemu-guest-agent 2>/dev/null"
|
|
|
|
|
" || systemctl enable qemu-ga 2>/dev/null || true",
|
|
|
|
|
# restart (et non enable --now) : recharge la config si l'agent était
|
|
|
|
|
# déjà démarré par l'image cloud.
|
|
|
|
|
" - systemctl restart qemu-guest-agent 2>/dev/null"
|
|
|
|
|
" || systemctl restart qemu-ga 2>/dev/null || true",
|
|
|
|
|
]
|
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# network-config (cloud-init v2) : DHCP sur toute interface « e* ». Les images
|
|
|
|
|
# Debian genericcloud ne configurent pas toujours le réseau sans ça (le NIC
|
|
|
|
|
# reste down -> pas d'IP), contrairement à Ubuntu. Inoffensif pour Ubuntu.
|
|
|
|
|
# La clé est « eth0 » car le renderer cloud-init d'Arch utilise la CLÉ comme
|
|
|
|
|
# nom d'interface (en ignorant « match ») et l'image Arch nomme son NIC eth0 ;
|
|
|
|
|
# Debian/Fedora/Ubuntu utilisent bien « match: name: e* » (leur en*).
|
|
|
|
|
NETWORK_CONFIG = (
|
|
|
|
|
"version: 2\n"
|
|
|
|
|
"ethernets:\n"
|
|
|
|
|
" eth0:\n"
|
|
|
|
|
" match:\n"
|
|
|
|
|
' name: "e*"\n'
|
|
|
|
|
" dhcp4: true\n"
|
|
|
|
|
" dhcp6: false\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_seed(
|
|
|
|
|
cloud_cfg: str, hostname: str, seed_dest: Path, runner: Runner
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Génère le seed.iso (cidata) et le copie vers seed_dest."""
|
|
|
|
|
meta_data = f"instance-id: {hostname}\nlocal-hostname: {hostname}\n"
|
|
|
|
|
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
tmp_path = Path(tmp)
|
|
|
|
|
ud = tmp_path / "user-data"
|
|
|
|
|
md = tmp_path / "meta-data"
|
|
|
|
|
nc = tmp_path / "network-config"
|
|
|
|
|
local_iso = tmp_path / "seed.iso"
|
|
|
|
|
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
print(" [dry-run] user-data qui serait généré :")
|
|
|
|
|
print(textwrap.indent(cloud_cfg, " "))
|
|
|
|
|
print(" [dry-run] network-config :")
|
|
|
|
|
print(textwrap.indent(NETWORK_CONFIG, " "))
|
|
|
|
|
else:
|
|
|
|
|
ud.write_text(cloud_cfg)
|
|
|
|
|
md.write_text(meta_data)
|
|
|
|
|
nc.write_text(NETWORK_CONFIG)
|
|
|
|
|
|
|
|
|
|
if runner.dry_run or shutil.which("cloud-localds"):
|
|
|
|
|
runner.run(
|
|
|
|
|
[
|
|
|
|
|
"cloud-localds",
|
|
|
|
|
"--network-config",
|
|
|
|
|
str(nc),
|
|
|
|
|
str(local_iso),
|
|
|
|
|
str(ud),
|
|
|
|
|
str(md),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
need_tool("genisoimage")
|
|
|
|
|
runner.run(
|
|
|
|
|
[
|
|
|
|
|
"genisoimage",
|
|
|
|
|
"-output",
|
|
|
|
|
str(local_iso),
|
|
|
|
|
"-volid",
|
|
|
|
|
"cidata",
|
|
|
|
|
"-joliet",
|
|
|
|
|
"-rock",
|
|
|
|
|
str(ud),
|
|
|
|
|
str(md),
|
|
|
|
|
str(nc),
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
seed_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
if not runner.dry_run
|
|
|
|
|
else None
|
|
|
|
|
)
|
|
|
|
|
runner.run(["cp", str(local_iso), str(seed_dest)], privileged=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prepare_disk(
|
|
|
|
|
image: Path, disk: Path, size: str, runner: Runner, force: bool
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Convertit l'image cloud en qcow2 de travail puis redimensionne."""
|
|
|
|
|
if disk.exists() and not force:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"Le disque {disk} existe déjà. Utilisez --force pour l'écraser."
|
|
|
|
|
)
|
|
|
|
|
runner.run(
|
|
|
|
|
[
|
|
|
|
|
"qemu-img",
|
|
|
|
|
"convert",
|
|
|
|
|
"-f",
|
|
|
|
|
"qcow2",
|
|
|
|
|
"-O",
|
|
|
|
|
"qcow2",
|
|
|
|
|
str(image),
|
|
|
|
|
str(disk),
|
|
|
|
|
],
|
|
|
|
|
privileged=True,
|
|
|
|
|
)
|
|
|
|
|
runner.run(["qemu-img", "resize", str(disk), size], privileged=True)
|
|
|
|
|
|
|
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
def build_preseed(
|
|
|
|
|
args: argparse.Namespace, pw_hash: str | None, ssh_keys: list[str]
|
|
|
|
|
) -> str:
|
|
|
|
|
"""Preseed debian-installer équivalent au cloud-config des autres distros.
|
|
|
|
|
|
|
|
|
|
Il doit couvrir EXACTEMENT ce que cloud-init fait ailleurs : nom d'hôte,
|
|
|
|
|
utilisateur, clés SSH, sudo sans mot de passe, fuseau, paquets de base.
|
|
|
|
|
Tout ce qui manque ici devient une question posée à l'écran, et
|
|
|
|
|
l'installation s'arrête sur une console que personne ne regarde.
|
|
|
|
|
|
|
|
|
|
« priority=critical » suffit à ne pas poser les questions restantes ; il
|
|
|
|
|
ne dispense PAS de répondre à celles qui n'ont pas de défaut, d'où le
|
|
|
|
|
partitionnement et le miroir écrits explicitement.
|
|
|
|
|
"""
|
|
|
|
|
user = args.user
|
|
|
|
|
# Sans mot de passe utilisable, d-i s'arrête sur la création du compte :
|
|
|
|
|
# « ! » est un hachage volontairement invalide — la connexion se fera par
|
|
|
|
|
# clé, comme le cloud-config le prévoit lui aussi.
|
|
|
|
|
crypted = pw_hash or "!"
|
|
|
|
|
lines = [
|
|
|
|
|
"d-i debian-installer/locale string en_US.UTF-8",
|
|
|
|
|
"d-i keyboard-configuration/xkb-keymap select us",
|
|
|
|
|
# « auto » évite la question du choix d'interface : sous virtio-ccw
|
|
|
|
|
# elle s'appelle enc1 et non eth0, et le nom n'est pas devinable.
|
|
|
|
|
"d-i netcfg/choose_interface select auto",
|
|
|
|
|
f"d-i netcfg/get_hostname string {args.hostname}",
|
|
|
|
|
"d-i netcfg/get_domain string localdomain",
|
|
|
|
|
"d-i netcfg/hostname string " + args.hostname,
|
|
|
|
|
"d-i mirror/country string manual",
|
|
|
|
|
"d-i mirror/http/hostname string deb.debian.org",
|
|
|
|
|
"d-i mirror/http/directory string /debian",
|
|
|
|
|
"d-i mirror/http/proxy string",
|
|
|
|
|
"d-i passwd/root-login boolean false",
|
|
|
|
|
"d-i passwd/user-fullname string ERPLibre",
|
|
|
|
|
f"d-i passwd/username string {user}",
|
|
|
|
|
f"d-i passwd/user-password-crypted password {crypted}",
|
|
|
|
|
"d-i clock-setup/utc boolean true",
|
|
|
|
|
f"d-i time/zone string {args.timezone}",
|
|
|
|
|
"d-i clock-setup/ntp boolean true",
|
|
|
|
|
# Le disque est nommé : sur s390x virtio-ccw il n'y en a qu'un, mais
|
|
|
|
|
# d-i pose quand même la question quand rien ne le désigne.
|
|
|
|
|
"d-i partman-auto/disk string /dev/vda",
|
|
|
|
|
"d-i partman-auto/method string regular",
|
|
|
|
|
"d-i partman-auto/choose_recipe select atomic",
|
|
|
|
|
"d-i partman/default_filesystem string ext4",
|
|
|
|
|
"d-i partman-partitioning/confirm_write_new_label boolean true",
|
|
|
|
|
"d-i partman/choose_partition select finish",
|
|
|
|
|
"d-i partman/confirm boolean true",
|
|
|
|
|
"d-i partman/confirm_nooverwrite boolean true",
|
|
|
|
|
"tasksel tasksel/first multiselect ssh-server",
|
|
|
|
|
"d-i pkgsel/include string openssh-server sudo python3"
|
|
|
|
|
" qemu-guest-agent ca-certificates",
|
|
|
|
|
"d-i pkgsel/upgrade select none",
|
|
|
|
|
"popularity-contest popularity-contest/participate boolean false",
|
|
|
|
|
"d-i finish-install/reboot_in_progress note",
|
|
|
|
|
]
|
|
|
|
|
# late_command : tout ce que le preseed ne sait pas exprimer. « in-target »
|
|
|
|
|
# exécute DANS le système installé ; les redirections, elles, restent dans
|
|
|
|
|
# l'installateur et doivent donc viser /target.
|
|
|
|
|
post = [
|
|
|
|
|
f"in-target usermod -aG sudo {user}",
|
|
|
|
|
f"echo '{user} ALL=(ALL) NOPASSWD:ALL' > /target/etc/sudoers.d/{user}",
|
|
|
|
|
f"chmod 440 /target/etc/sudoers.d/{user}",
|
|
|
|
|
]
|
|
|
|
|
if ssh_keys:
|
|
|
|
|
post.append(f"mkdir -p /target/home/{user}/.ssh")
|
|
|
|
|
for key in ssh_keys:
|
|
|
|
|
post.append(
|
|
|
|
|
f"echo '{key}' >> /target/home/{user}/.ssh/authorized_keys"
|
|
|
|
|
)
|
|
|
|
|
post += [
|
|
|
|
|
f"in-target chown -R {user}:{user} /home/{user}/.ssh",
|
|
|
|
|
f"chmod 700 /target/home/{user}/.ssh",
|
|
|
|
|
f"chmod 600 /target/home/{user}/.ssh/authorized_keys",
|
|
|
|
|
]
|
|
|
|
|
lines.append("d-i preseed/late_command string " + " ; ".join(post))
|
|
|
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_installer_initrd(
|
|
|
|
|
preseed: str, initrd_src: Path, out: Path, runner: Runner
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Glisse le preseed DANS l'initrd de l'installateur.
|
|
|
|
|
|
|
|
|
|
Servir le preseed en HTTP est l'autre voie documentée, mais elle ajoute un
|
|
|
|
|
serveur à faire vivre pendant toute l'installation et une dépendance à
|
|
|
|
|
l'ordre d'obtention de l'adresse. Embarquer le fichier ne dépend de rien :
|
|
|
|
|
d-i lit « /preseed.cfg » à la racine de l'initrd avant même le réseau.
|
|
|
|
|
|
|
|
|
|
La méthode est celle de la documentation Debian — décompresser, ajouter le
|
|
|
|
|
fichier au cpio, recompresser — et non une concaténation d'archives, que
|
|
|
|
|
le noyau accepte mais que d-i ne parcourt pas de la même façon.
|
|
|
|
|
"""
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
print(f"[dry-run] preseed -> {out}")
|
|
|
|
|
return
|
|
|
|
|
if not shutil.which("cpio"):
|
|
|
|
|
sys.exit(
|
|
|
|
|
"cpio est requis pour embarquer le preseed dans l'initrd.\n"
|
|
|
|
|
" Debian/Ubuntu : sudo apt-get install cpio"
|
|
|
|
|
)
|
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
|
|
|
work = Path(tmp)
|
|
|
|
|
(work / "preseed.cfg").write_text(preseed, encoding="utf-8")
|
|
|
|
|
plain = work / "initrd"
|
|
|
|
|
with gzip.open(initrd_src, "rb") as src, open(plain, "wb") as dst:
|
|
|
|
|
shutil.copyfileobj(src, dst)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["cpio", "-H", "newc", "-o", "-A", "-F", str(plain)],
|
|
|
|
|
input="preseed.cfg\n",
|
|
|
|
|
text=True,
|
|
|
|
|
cwd=work,
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
)
|
|
|
|
|
out.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
with open(plain, "rb") as src, gzip.open(out, "wb") as dst:
|
|
|
|
|
shutil.copyfileobj(src, dst)
|
|
|
|
|
print(f" preseed embarqué dans {out} ({out.stat().st_size} octets)")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_blank_disk(
|
|
|
|
|
disk: Path, size: str, runner: Runner, force: bool
|
|
|
|
|
) -> None:
|
|
|
|
|
"""Disque VIERGE : l'installateur écrit tout, il n'y a rien à convertir."""
|
|
|
|
|
if disk.exists() and not force:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"Le disque {disk} existe déjà. Utilisez --force pour l'écraser."
|
|
|
|
|
)
|
|
|
|
|
runner.run(
|
|
|
|
|
["qemu-img", "create", "-f", "qcow2", str(disk), size],
|
|
|
|
|
privileged=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
def network_name(network_arg: str) -> str | None:
|
|
|
|
|
"""Extrait NAME de « network=NAME,... » ; None si c'est un bridge, etc."""
|
|
|
|
|
for part in network_arg.split(","):
|
|
|
|
|
if part.strip().startswith("network="):
|
|
|
|
|
return part.split("=", 1)[1].strip()
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def network_state(name: str, use_sudo: bool) -> tuple[bool, bool]:
|
|
|
|
|
"""(actif, autostart) d'un réseau libvirt, via « virsh net-info »."""
|
|
|
|
|
cmd = (["sudo"] if use_sudo else []) + [
|
|
|
|
|
"virsh",
|
|
|
|
|
"-c",
|
|
|
|
|
LIBVIRT_URI,
|
|
|
|
|
"net-info",
|
|
|
|
|
name,
|
|
|
|
|
]
|
|
|
|
|
try:
|
|
|
|
|
res = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
return (False, False)
|
|
|
|
|
active = bool(re.search(r"Active:\s*yes", res.stdout, re.IGNORECASE))
|
|
|
|
|
autostart = bool(re.search(r"Autostart:\s*yes", res.stdout, re.IGNORECASE))
|
|
|
|
|
return (active, autostart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_network(name: str | None, runner: Runner) -> None:
|
|
|
|
|
"""Active le réseau libvirt si besoin. On vérifie d'abord son état pour
|
|
|
|
|
éviter le faux « error: network is already active » de virsh quand il
|
|
|
|
|
tourne déjà (message purement bruyant, sans conséquence)."""
|
|
|
|
|
if not name:
|
|
|
|
|
return
|
|
|
|
|
if runner.dry_run:
|
|
|
|
|
print(f" [dry-run] réseau libvirt '{name}' activé si nécessaire")
|
|
|
|
|
runner.run(
|
|
|
|
|
["virsh", "-c", LIBVIRT_URI, "net-start", name],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
runner.run(
|
|
|
|
|
["virsh", "-c", LIBVIRT_URI, "net-autostart", name],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
active, autostart = network_state(name, runner.use_sudo)
|
|
|
|
|
if active and autostart:
|
|
|
|
|
print(f" Réseau libvirt '{name}' déjà actif.")
|
|
|
|
|
return
|
|
|
|
|
print(f" Configuration du réseau libvirt '{name}'…")
|
|
|
|
|
if not active:
|
|
|
|
|
runner.run(
|
|
|
|
|
["virsh", "-c", LIBVIRT_URI, "net-start", name],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
if not autostart:
|
|
|
|
|
runner.run(
|
|
|
|
|
["virsh", "-c", LIBVIRT_URI, "net-autostart", name],
|
|
|
|
|
privileged=True,
|
|
|
|
|
check=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def virt_install(
|
|
|
|
|
args: argparse.Namespace,
|
|
|
|
|
disk: Path,
|
|
|
|
|
seed: Path,
|
|
|
|
|
osinfo: str,
|
|
|
|
|
runner: Runner,
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
installer: tuple[Path, Path] | None = None,
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
) -> None:
|
|
|
|
|
# Émulée (TCG, pas de KVM) si l'arch demandée diffère de celle de l'hôte.
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
# Deux causes d'émulation, à ne pas confondre : une architecture étrangère
|
|
|
|
|
# (voulue, on la choisit), et une absence de KVM (subie, et invisible). La
|
|
|
|
|
# seconde ne se déduit PAS de l'architecture — dans une VM sans
|
|
|
|
|
# virtualisation imbriquée, libvirt retombe en TCG sans rien dire.
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
emulated = args.arch != host_arch()
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
if not emulated and not kvm_available():
|
|
|
|
|
emulated = True
|
|
|
|
|
print(
|
|
|
|
|
"⚠ KVM indisponible (/dev/kvm) : cette VM sera ÉMULÉE, donc TRÈS"
|
|
|
|
|
" lente."
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
" Cause habituelle : l'hôte est lui-même une VM sans"
|
|
|
|
|
" virtualisation imbriquée."
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
" Vérifier : systemd-detect-virt et"
|
|
|
|
|
' virsh capabilities | grep "domain type"'
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
# s390x n'a pas de port série ISA : la console est SCLP (ttysclp0), et non
|
|
|
|
|
# ttyS0. Ailleurs (x86/arm64), console série classique.
|
|
|
|
|
console_target = "sclp" if args.arch == "s390x" else "serial"
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
# Écran virtuel pour une VM graphique. s390x en est écarté : QEMU y expose
|
|
|
|
|
# bien « virtio-gpu-ccw », mais rien ne garantit que le noyau s390x de la
|
|
|
|
|
# distribution embarque le pilote DRM virtio-gpu — la VM démarrerait alors
|
|
|
|
|
# sur un écran noir. Le bureau distant, lui, marche partout : c'est la voie
|
|
|
|
|
# retenue pour cette architecture, et le message le dit.
|
|
|
|
|
graphics = args.graphics
|
|
|
|
|
video = []
|
|
|
|
|
if args.desktop and graphics == "none":
|
|
|
|
|
if args.arch == "s390x":
|
|
|
|
|
print(
|
|
|
|
|
"\n s390x : pas d'écran virtuel (pilote DRM non garanti)."
|
|
|
|
|
"\n Le bureau sera accessible à distance, par le réseau."
|
|
|
|
|
)
|
|
|
|
|
else:
|
[ADD] qemu: build the tunnel to the remote desktop
A "Remote desktop tunnel" entry under SSH configuration. It lists the
targets, resolves them, and composes the full command -- nothing to fill
in.
todo.py runs on the libvirt HOST; the tunnel starts from the workstation.
It cannot open it, but it alone knows the VM's private IP, its port and
the address through which it was reached. That last one comes from
SSH_CONNECTION, whose third field is exactly the server address used --
far safer than a "hostname" that may resolve to nothing outside.
When ~/.ssh/config already carries the entry, the tunnel takes it:
"ssh -N -L 5902:localhost:5901 <vm>". ProxyJump makes the route and
"localhost" means the VM's OWN loopback, so the tunnel survives an IP
change. That config is also the right source of targets, not the local
libvirt: a graphical VM is often nested, and virsh would only ever list
the orchestrator.
Two details: the hypervisor console needs VNC bound to the loopback, as
"listen=none" opens no socket at all; and two menu entries had no icon.
--- FR ---
Une entrée « Tunnel bureau distant » sous Configuration SSH. Elle liste
les cibles, les résout, et compose la commande complète — rien à remplir.
todo.py tourne sur l'HÔTE libvirt ; le tunnel, lui, part du poste de
travail. Il ne peut donc pas l'ouvrir, mais il est le seul à connaître
l'IP privée de la VM, son port et l'adresse par laquelle on l'a joint.
Cette dernière vient de SSH_CONNECTION, dont le troisième champ est
exactement l'adresse serveur utilisée — bien plus sûr qu'un « hostname »
qui peut ne rien résoudre depuis l'extérieur.
Quand ~/.ssh/config porte déjà l'entrée, le tunnel l'emprunte :
« ssh -N -L 5902:localhost:5901 <vm> ». Le ProxyJump fait la route et
« localhost » désigne le bouclage DE LA VM, si bien que le tunnel survit à
un changement d'IP. Ce fichier est aussi la bonne source de cibles, pas le
libvirt local : une VM graphique est souvent imbriquée, et virsh ne
listerait jamais que l'orchestrateur.
Deux détails : la console de l'hyperviseur exige un VNC sur la boucle
locale, « listen=none » n'ouvrant aucun socket ; et deux entrées de menu
n'avaient pas d'icône.
Assisted-by: Claude Opus 5
2026-08-13 03:40:22 -04:00
|
|
|
# VNC sur la boucle locale, PAS « spice,listen=none ».
|
|
|
|
|
#
|
|
|
|
|
# « listen=none » est le défaut de virt-install et il n'expose
|
|
|
|
|
# rien : QEMU crée l'affichage mais n'ouvre AUCUN socket TCP, seul
|
|
|
|
|
# le canal libvirt y mène. Un virt-manager tournant sur la machine
|
|
|
|
|
# même y accède ; rien d'autre. Or l'hôte QEMU est lui-même une VM
|
|
|
|
|
# ici — la console était donc inatteignable par construction, et
|
|
|
|
|
# aucun « ssh -L » ne pouvait y remédier : il n'y avait pas de port
|
|
|
|
|
# où aboutir.
|
|
|
|
|
#
|
|
|
|
|
# 127.0.0.1 n'expose rien au réseau non plus : le port n'est
|
|
|
|
|
# joignable que depuis l'hôte, donc à travers un tunnel SSH. VNC
|
|
|
|
|
# plutôt que SPICE parce qu'il tient en UN port — un seul « -L »
|
|
|
|
|
# suffit, avec n'importe quel client. Pour revenir au comportement
|
|
|
|
|
# d'avant : --graphics spice,listen=none
|
|
|
|
|
graphics = "vnc,listen=127.0.0.1"
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
video = ["--video", "virtio"]
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
cmd = [
|
|
|
|
|
"virt-install",
|
|
|
|
|
# Sans --connect, un utilisateur non root vise qemu:///session : le
|
|
|
|
|
# réseau « default » n'y existe pas et la création échoue même quand
|
|
|
|
|
# tous les paquets sont installés et le réseau actif côté système.
|
|
|
|
|
"--connect",
|
|
|
|
|
LIBVIRT_URI,
|
|
|
|
|
"--name",
|
|
|
|
|
args.name,
|
|
|
|
|
"--memory",
|
|
|
|
|
str(args.memory),
|
|
|
|
|
"--vcpus",
|
|
|
|
|
str(args.vcpus),
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
]
|
|
|
|
|
if installer:
|
|
|
|
|
kernel, initrd = installer
|
|
|
|
|
# « --install » et non « --boot » : virt-install écrit alors DEUX
|
|
|
|
|
# configurations — celle de l'installation, transitoire, et celle du
|
|
|
|
|
# système installé. Avec « --boot kernel=… » la VM repartirait sur
|
|
|
|
|
# l'installateur à chaque démarrage, indéfiniment.
|
|
|
|
|
#
|
|
|
|
|
# console=ttysclp0 : s390x n'a pas de port série ISA. Sans cet
|
|
|
|
|
# argument l'installateur tourne sur une console invisible, et un
|
|
|
|
|
# échec ne laisse aucune trace lisible.
|
|
|
|
|
cmd += [
|
|
|
|
|
"--install",
|
|
|
|
|
f"kernel={kernel},initrd={initrd},"
|
|
|
|
|
"kernel_args=auto=true priority=critical "
|
|
|
|
|
"preseed/file=/preseed.cfg console=ttysclp0",
|
|
|
|
|
]
|
|
|
|
|
else:
|
|
|
|
|
cmd.append("--import")
|
|
|
|
|
cmd += [
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
"--disk",
|
|
|
|
|
f"path={disk},format=qcow2,bus=virtio",
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
]
|
|
|
|
|
# Le seed n'existe QUE sur la voie image cloud. Sous debian-installer, le
|
|
|
|
|
# preseed voyage dans l'initrd et un second disque ne ferait qu'ajouter un
|
|
|
|
|
# /dev/vdb dont partman-auto devrait être protégé.
|
|
|
|
|
if not installer:
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
# Seed cloud-init attaché comme DISQUE virtio en lecture seule (et non
|
|
|
|
|
# en CD-ROM) : le pilote virtio-blk est dans l'initramfs, donc le
|
|
|
|
|
# volume « cidata » est visible dès init-local et cloud-init le lit.
|
|
|
|
|
# En CD-ROM, l'initramfs Debian ne charge pas sr_mod à temps -> le
|
|
|
|
|
# seed n'est pas vu et rien ne s'applique (Ubuntu, lui, tolère le CD).
|
|
|
|
|
# Sur s390x, bus=virtio est mappé en virtio-ccw par libvirt.
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
cmd += ["--disk", f"path={seed},readonly=on,bus=virtio"]
|
|
|
|
|
cmd += [
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
"--osinfo",
|
|
|
|
|
osinfo,
|
|
|
|
|
"--network",
|
|
|
|
|
args.network,
|
|
|
|
|
"--graphics",
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
graphics,
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
"--console",
|
|
|
|
|
f"pty,target_type={console_target}",
|
|
|
|
|
# Canal virtio de l'agent invité (org.qemu.guest_agent.0) : permet à
|
|
|
|
|
# virsh de piloter la VM SANS réseau (ex. étendre le FS invité après
|
|
|
|
|
# un redimensionnement de disque). Inoffensif si l'agent est absent.
|
|
|
|
|
"--channel",
|
|
|
|
|
"unix,target.type=virtio,target.name=org.qemu.guest_agent.0",
|
|
|
|
|
]
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
cmd += video
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
if args.arch == "s390x":
|
|
|
|
|
# s390x (IBM Z) : machine s390-ccw-virtio, amorçage IPL/zipl depuis le
|
|
|
|
|
# disque (ni BIOS ni UEFI/OVMF -> aucun --boot).
|
|
|
|
|
cmd += ["--arch", "s390x", "--machine", "s390-ccw-virtio"]
|
|
|
|
|
elif args.arch == "arm64":
|
|
|
|
|
# arm64/aarch64 : machine « virt » + UEFI (firmware AAVMF). Les images
|
|
|
|
|
# cloud arm64 n'ont pas de BIOS -> UEFI obligatoire. Secure Boot
|
|
|
|
|
# DÉSACTIVÉ (comme x86) : sinon libvirt sélectionne l'AAVMF « secure »
|
|
|
|
|
# à clés Microsoft enrôlées et la VM RESTE FIGÉE au firmware (aucun
|
|
|
|
|
# boot, aucune IP). secure-boot=no -> AAVMF non-SB, boot OK.
|
|
|
|
|
cmd += [
|
|
|
|
|
"--arch",
|
|
|
|
|
"aarch64",
|
|
|
|
|
"--machine",
|
|
|
|
|
"virt",
|
|
|
|
|
"--boot",
|
|
|
|
|
"uefi,firmware.feature0.name=secure-boot,"
|
|
|
|
|
"firmware.feature0.enabled=no",
|
|
|
|
|
]
|
|
|
|
|
elif not args.bios:
|
|
|
|
|
# Boot UEFI par défaut (x86) : Debian 13 (trixie) et les images cloud
|
|
|
|
|
# récentes n'embarquent plus le chargeur BIOS/GRUB-pc et partent en
|
|
|
|
|
# boucle « Booting... » en SeaBIOS. UEFI (OVMF) fonctionne pour
|
|
|
|
|
# Ubuntu/Debian/Fedora. --bios force l'ancien BIOS si OVMF est absent.
|
|
|
|
|
# Secure Boot DÉSACTIVÉ : le chargeur d'Arch (GRUB) n'est pas signé et
|
|
|
|
|
# OVMF Secure Boot le refuse (« Access Denied » -> pas de boot).
|
|
|
|
|
# Ubuntu/Debian/Fedora bootent aussi sans Secure Boot.
|
|
|
|
|
cmd += [
|
|
|
|
|
"--boot",
|
|
|
|
|
"uefi,firmware.feature0.name=secure-boot,"
|
|
|
|
|
"firmware.feature0.enabled=no",
|
|
|
|
|
]
|
|
|
|
|
if emulated:
|
|
|
|
|
# Architecture différente de l'hôte -> émulation logicielle TCG
|
|
|
|
|
# (pas de KVM). LENT.
|
|
|
|
|
cmd += ["--virt-type", "qemu"]
|
|
|
|
|
if not args.attach_console:
|
|
|
|
|
cmd.append("--noautoconsole")
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
if installer:
|
|
|
|
|
# Sans « --wait 0 », virt-install RESTE au premier plan jusqu'à la
|
|
|
|
|
# fin de l'installation. Sous émulation s390x elle se compte en
|
|
|
|
|
# heures, et le déploiement parallèle attendrait chaque VM l'une
|
|
|
|
|
# après l'autre. La configuration finale est déjà écrite : rendre
|
|
|
|
|
# la main n'abandonne rien.
|
|
|
|
|
cmd += ["--wait", "0"]
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
# virtinst écrit un journal de debug dans ~/.cache/virt-manager ; sous
|
|
|
|
|
# sudo, HOME/cache peut être inaccessible -> l'écriture échoue et Python
|
|
|
|
|
# déverse un « Logging error » (le pavé « Fetched capabilities … »). On
|
|
|
|
|
# force un cache/HOME ÉCRIVABLE via « env VAR=… » (traverse sudo) pour
|
|
|
|
|
# que le journal s'écrive silencieusement.
|
|
|
|
|
# Chemin propre à l'UID : un répertoire partagé finit créé par root lors
|
|
|
|
|
# d'un premier passage sous sudo, puis devient illisible pour l'utilisateur
|
|
|
|
|
# (« Error setting up logfile: No write access to … /virt-manager »).
|
|
|
|
|
cache_dir = f"/var/tmp/erplibre-virtinst-{os.getuid()}"
|
|
|
|
|
try:
|
|
|
|
|
os.makedirs(cache_dir, mode=0o700, exist_ok=True)
|
|
|
|
|
except OSError:
|
|
|
|
|
pass
|
|
|
|
|
log_env = [
|
|
|
|
|
"env",
|
|
|
|
|
f"XDG_CACHE_HOME={cache_dir}",
|
|
|
|
|
f"HOME={cache_dir}",
|
|
|
|
|
]
|
|
|
|
|
runner.run(log_env + cmd, privileged=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ip_reachable(ip: str, port: int = 22, timeout: float = 3) -> bool:
|
|
|
|
|
"""Vrai si le port SSH répond (distingue le bail actif du bail périmé)."""
|
|
|
|
|
try:
|
|
|
|
|
with socket.create_connection((ip, port), timeout=timeout):
|
|
|
|
|
return True
|
|
|
|
|
except OSError:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wait_for_ip(name: str, use_sudo: bool, timeout: int) -> str | None:
|
|
|
|
|
"""Interroge les baux DHCP libvirt jusqu'à obtenir l'IPv4 de la VM. Une VM
|
|
|
|
|
peut avoir PLUSIEURS baux (l'image demande d'abord une IP avec le hostname
|
|
|
|
|
par défaut « ubuntu », puis cloud-init fixe le vrai hostname -> 2e bail) :
|
|
|
|
|
on renvoie une IP JOIGNABLE (sshd up), sinon la plus récente, jamais
|
|
|
|
|
aveuglément la 1re (souvent le bail précoce périmé, « No route to host »).
|
|
|
|
|
"""
|
|
|
|
|
base = (["sudo"] if use_sudo else []) + [
|
|
|
|
|
"virsh",
|
|
|
|
|
"domifaddr",
|
|
|
|
|
name,
|
|
|
|
|
"--source",
|
|
|
|
|
"lease",
|
|
|
|
|
]
|
|
|
|
|
deadline = time.time() + timeout
|
|
|
|
|
ips: list[str] = []
|
|
|
|
|
while time.time() < deadline:
|
|
|
|
|
res = subprocess.run(base, capture_output=True, text=True)
|
|
|
|
|
ips = re.findall(r"(\d+\.\d+\.\d+\.\d+)", res.stdout)
|
|
|
|
|
for ip in ips:
|
|
|
|
|
if _ip_reachable(ip):
|
|
|
|
|
return ip
|
|
|
|
|
time.sleep(3)
|
|
|
|
|
return ips[-1] if ips else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def ssh_command(user: str, ip: str, has_key: bool) -> str:
|
|
|
|
|
"""Commande SSH adaptée : clé -> connexion simple ; mot de passe seul ->
|
|
|
|
|
force le mot de passe pour éviter « Too many authentication failures »."""
|
|
|
|
|
if has_key:
|
|
|
|
|
return f"ssh {user}@{ip}"
|
|
|
|
|
return (
|
|
|
|
|
f"ssh -o IdentitiesOnly=yes -o PreferredAuthentications=password "
|
|
|
|
|
f"{user}@{ip}"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
# CLI
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
|
def build_parser() -> argparse.ArgumentParser:
|
|
|
|
|
versions_help = "\n".join(
|
|
|
|
|
f" {distro:<7} {default:<7} (défaut) versions : "
|
|
|
|
|
+ ", ".join(versions)
|
|
|
|
|
for distro, (versions, default) in DISTROS.items()
|
|
|
|
|
)
|
|
|
|
|
p = argparse.ArgumentParser(
|
|
|
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
|
|
|
description=__doc__,
|
|
|
|
|
epilog="Distros et versions (--distro / --version), specs via "
|
|
|
|
|
"--list-images :\n" + versions_help,
|
|
|
|
|
)
|
|
|
|
|
p.add_argument(
|
|
|
|
|
"image_path",
|
|
|
|
|
type=Path,
|
|
|
|
|
nargs="?",
|
|
|
|
|
default=None,
|
|
|
|
|
help="Chemin de cache de l'image cloud. Optionnel : si absent, il est "
|
|
|
|
|
"déduit de --distro/--version/--codename + --arch dans --image-dir. "
|
|
|
|
|
"Si le fichier existe, il n'est PAS re-téléchargé.",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
g_img = p.add_argument_group("Image")
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--distro",
|
|
|
|
|
default="ubuntu",
|
|
|
|
|
choices=DISTROS,
|
|
|
|
|
help="Distribution : ubuntu, debian ou fedora (défaut : ubuntu).",
|
|
|
|
|
)
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--version",
|
|
|
|
|
default=None,
|
|
|
|
|
help="Version de la distro (défaut : la version par défaut de la "
|
|
|
|
|
"distro). Voir --list-images pour la liste complète.",
|
|
|
|
|
)
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--codename",
|
|
|
|
|
help="Force le nom de code / la release (surcharge --version).",
|
|
|
|
|
)
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--arch",
|
|
|
|
|
default="amd64",
|
|
|
|
|
help="Architecture de l'image (défaut : amd64). amd64/x86_64, "
|
|
|
|
|
"arm64/aarch64 (Ubuntu/Debian/Fedora) ou s390x (Ubuntu). Toute arch "
|
|
|
|
|
"différente de l'hôte est ÉMULÉE (TCG, lente).",
|
|
|
|
|
)
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--image-dir",
|
|
|
|
|
type=Path,
|
|
|
|
|
default=DEFAULT_IMAGE_DIR,
|
|
|
|
|
help="Répertoire de cache des images cloud quand image_path est "
|
|
|
|
|
f"omis (défaut : {DEFAULT_IMAGE_DIR}).",
|
|
|
|
|
)
|
|
|
|
|
g_img.add_argument(
|
|
|
|
|
"--verify",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Vérifie l'empreinte SHA256 après téléchargement (recommandé).",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
g_vm = p.add_argument_group("VM")
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--name",
|
|
|
|
|
help="Nom de la VM (virsh). Requis pour déployer ; inutile avec "
|
|
|
|
|
"--download-only.",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--hostname", help="Nom d'hôte interne (défaut : --name)."
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--memory",
|
|
|
|
|
type=int,
|
|
|
|
|
default=None,
|
|
|
|
|
help="RAM en Mo (défaut : minimum requis par la version choisie, "
|
|
|
|
|
"voir --list-images).",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--vcpus", type=int, default=2, help="Nombre de vCPU (défaut : 2)."
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--disk-size",
|
|
|
|
|
default=None,
|
|
|
|
|
help="Taille du disque virtuel, ex. 120G (défaut : minimum requis "
|
|
|
|
|
"par la version choisie, voir --list-images).",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--disk-dir",
|
|
|
|
|
type=Path,
|
|
|
|
|
default=Path("/var/lib/libvirt/images"),
|
|
|
|
|
help="Répertoire du qcow2 de travail (défaut : /var/lib/libvirt/images).",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--seed-dir",
|
|
|
|
|
type=Path,
|
|
|
|
|
default=Path("/var/lib/libvirt/images/iso"),
|
|
|
|
|
help="Répertoire du seed.iso (défaut : .../images/iso).",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--network",
|
|
|
|
|
default="network=default,model=virtio",
|
|
|
|
|
help="Argument --network de virt-install.",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--graphics",
|
|
|
|
|
default="none",
|
|
|
|
|
help="Argument --graphics (défaut : none).",
|
|
|
|
|
)
|
[ADD] tui qemu: graphical VMs, with a GNOME desktop
VMs could only be servers. A server-or-graphical choice joins both
interfaces, installing GNOME along with remote access to it.
Packages come from the remote command, not cloud-init: their 1 to 2 GB
would stretch an already long boot there while leaving no trace in the
monitoring, and group installs do not go through it. The desktop
therefore does not depend on ERPLibre -- a VM may be wanted graphical
and bare.
Each distribution has its own names, taken from the source: Arch has no
xrdp in its official repositories and takes TigerVNC. The SPICE display
is set only on amd64 and arm64; s390x does expose virtio-gpu-ccw, but
nothing guarantees its kernel's DRM driver, whereas remote desktop works
everywhere.
--- FR ---
Les VM ne pouvaient être que des serveurs. Un choix serveur ou graphique
s'ajoute aux deux interfaces, et pose GNOME avec son accès distant.
Les paquets viennent de la commande distante, pas de cloud-init : leurs
1 à 2 Go y allongeraient un démarrage déjà long sans laisser de trace
dans le suivi, et les installations par groupe n'y passent pas. Le bureau
ne dépend donc pas d'ERPLibre — une VM peut être voulue graphique et nue.
Chaque distribution a ses noms, relevés à la source : Arch n'a pas xrdp
dans ses dépôts officiels et prend TigerVNC. L'écran virtuel SPICE n'est
posé que sur amd64 et arm64 ; s390x expose bien virtio-gpu-ccw, mais rien
ne garantit le pilote DRM de son noyau, alors que le bureau distant, lui,
marche partout.
Assisted-by: Claude Opus 5
2026-08-11 09:49:17 -04:00
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--desktop",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="VM graphique : écran virtuel SPICE là où l'architecture le "
|
|
|
|
|
"permet. Les paquets GNOME sont posés par la commande d'installation, "
|
|
|
|
|
"pas ici.",
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--osinfo", help="Force la valeur --osinfo (sinon déduite)."
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--attach-console",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Attache la console série (sinon --noautoconsole).",
|
|
|
|
|
)
|
|
|
|
|
g_vm.add_argument(
|
|
|
|
|
"--bios",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Force l'amorçage BIOS hérité au lieu d'UEFI (par défaut UEFI ; "
|
|
|
|
|
"n'utiliser que si le firmware OVMF est absent).",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
g_cloud = p.add_argument_group("cloud-init")
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--user",
|
|
|
|
|
default="erplibre",
|
|
|
|
|
help="Utilisateur créé (défaut : erplibre).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--password", help="Mot de passe en clair (déconseillé : visible)."
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--ask-password",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Demande le mot de passe de façon interactive (sûr).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--password-hash",
|
|
|
|
|
help="Empreinte $6$... déjà calculée (openssl passwd -6).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--ssh-key",
|
|
|
|
|
action="append",
|
|
|
|
|
default=[],
|
|
|
|
|
metavar="FICHIER",
|
|
|
|
|
help="Fichier de clé publique SSH à injecter (répétable).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--locale",
|
|
|
|
|
default="fr_CA.UTF-8",
|
|
|
|
|
help="Locale (défaut : fr_CA.UTF-8).",
|
|
|
|
|
)
|
[IMP] qemu: a VM one can reach, and a first boot that does not stall
The monitor froze the address given at launch, so it lost the VM as soon as
cloud-init renamed the host and DHCP handed out another lease. It now
re-resolves at each attempt, in the views too, and reads virsh without sudo
— « sudo -n » fails in a detached session with no tty.
First boot also stopped paying for what it does not need: the guest agent
leaves cloud-init, snapd and locale-gen go, apt takes the fastest mirror.
The timezone follows the host. And when KVM is missing, the deployment says
so before the wait instead of being mysteriously fifteen times slower.
--- FR ---
Le suivi figeait l'adresse connue au lancement : il perdait donc la VM dès
que cloud-init posait le vrai nom d'hôte et que DHCP donnait un autre bail.
Il la ré-résout désormais à chaque tentative, dans les vues aussi, et lit
virsh sans sudo — « sudo -n » échoue dans une session détachée, sans tty.
Le premier démarrage cesse aussi de payer l'inutile : l'agent invité sort
de cloud-init, snapd et locale-gen disparaissent, apt prend le miroir le
plus rapide. Le fuseau suit l'hôte. Et faute de KVM, le déploiement le dit
avant l'attente, au lieu d'être quinze fois plus lent sans raison visible.
Assisted-by: Claude Opus 5
2026-08-10 03:10:50 -04:00
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--apt-mirror",
|
|
|
|
|
metavar="URI",
|
|
|
|
|
help=(
|
|
|
|
|
"Miroir apt unique (Ubuntu). Par défaut, cloud-init essaie dans"
|
|
|
|
|
" l'ordre les miroirs les plus rapides mesurés puis le dépôt"
|
|
|
|
|
" officiel."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--timezone",
|
|
|
|
|
default=host_timezone(),
|
|
|
|
|
metavar="ZONE",
|
|
|
|
|
help=(
|
|
|
|
|
"Fuseau horaire de la VM, au format zoneinfo "
|
|
|
|
|
f"(défaut : celui de l'hôte, {host_timezone()})."
|
|
|
|
|
),
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--keyboard-layout",
|
|
|
|
|
default="ca",
|
|
|
|
|
help="Disposition clavier (défaut : ca).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--keyboard-variant",
|
|
|
|
|
default="multix",
|
|
|
|
|
help="Variante clavier (défaut : multix).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--package",
|
|
|
|
|
action="append",
|
|
|
|
|
default=[],
|
|
|
|
|
metavar="PKG",
|
|
|
|
|
help="Paquet APT additionnel (répétable).",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--no-upgrade",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="N'exécute pas package_upgrade au premier boot.",
|
|
|
|
|
)
|
|
|
|
|
g_cloud.add_argument(
|
|
|
|
|
"--apt-update",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Exécute « apt update » au 1er boot (package_update). Désactivé "
|
|
|
|
|
"par défaut : évite que cloud-init se bloque sur un miroir lent/"
|
|
|
|
|
"injoignable (SSH est déjà présent dans les images cloud).",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
g_run = p.add_argument_group("Exécution")
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--no-sudo",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="N'utilise pas sudo pour les étapes privilégiées.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--force",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Écrase le qcow2 de travail existant.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--no-wait-ip",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="N'attend pas l'attribution de l'IP à la fin.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--ip-timeout",
|
|
|
|
|
type=int,
|
|
|
|
|
default=90,
|
|
|
|
|
metavar="SEC",
|
|
|
|
|
help="Délai max d'attente de l'IP DHCP (défaut : 90 s).",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--dry-run",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Affiche les commandes et le user-data sans rien exécuter.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--download-only",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Télécharge (et vérifie si --verify) l'image cloud puis quitte, "
|
|
|
|
|
"sans créer de VM. --name n'est pas requis.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"-y",
|
|
|
|
|
"--assume-yes",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Accepte automatiquement l'installation des dépendances manquantes.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--no-install-deps",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="N'installe jamais les dépendances manquantes (échoue si absentes).",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--setup-host",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Prépare l'hôte (paquets QEMU/libvirt, démon, groupe libvirt, "
|
|
|
|
|
"réseau default) puis quitte. Ne déploie aucune VM.",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--reboot-if-needed",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Avec --setup-host : redémarre si le noyau a été mis à jour "
|
|
|
|
|
"depuis le démarrage (sinon libvirt ne peut pas créer virbr0).",
|
|
|
|
|
)
|
|
|
|
|
g_run.add_argument(
|
|
|
|
|
"--list-images",
|
|
|
|
|
action="store_true",
|
|
|
|
|
help="Liste les distros/versions disponibles et leurs specs, "
|
|
|
|
|
"puis quitte.",
|
|
|
|
|
)
|
|
|
|
|
return p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resolve_password(args: argparse.Namespace) -> str | None:
|
|
|
|
|
if args.password_hash:
|
|
|
|
|
return args.password_hash
|
|
|
|
|
if args.ask_password:
|
|
|
|
|
pw = getpass.getpass("Mot de passe pour l'utilisateur : ")
|
|
|
|
|
if pw != getpass.getpass("Confirmer : "):
|
|
|
|
|
sys.exit("Les mots de passe ne correspondent pas.")
|
|
|
|
|
return hash_password(pw)
|
|
|
|
|
if args.password:
|
|
|
|
|
return hash_password(args.password)
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def load_ssh_keys(paths: list[str]) -> list[str]:
|
|
|
|
|
keys: list[str] = []
|
|
|
|
|
for path in paths:
|
|
|
|
|
p = Path(path).expanduser()
|
|
|
|
|
if not p.exists():
|
|
|
|
|
sys.exit(f"Clé SSH introuvable : {p}")
|
|
|
|
|
keys.append(p.read_text().strip())
|
|
|
|
|
return keys
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
# Sortie ligne par ligne même quand stdout est un tube (menu todo) : sinon
|
|
|
|
|
# les en-têtes restent bufferisés et le déploiement paraît « gelé ».
|
|
|
|
|
try:
|
|
|
|
|
sys.stdout.reconfigure(line_buffering=True)
|
|
|
|
|
except (AttributeError, ValueError):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
args = build_parser().parse_args()
|
|
|
|
|
|
|
|
|
|
if args.list_images:
|
|
|
|
|
list_images()
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Préparation de l'hôte : aucune image, aucune distro cible, aucun --name.
|
|
|
|
|
# Traité AVANT la résolution de version/image pour rester utilisable seul.
|
|
|
|
|
if args.setup_host:
|
|
|
|
|
setup_host(
|
|
|
|
|
Runner(
|
|
|
|
|
use_sudo=not args.no_sudo and os.geteuid() != 0,
|
|
|
|
|
dry_run=args.dry_run,
|
|
|
|
|
),
|
|
|
|
|
args.assume_yes,
|
|
|
|
|
args.no_install_deps,
|
|
|
|
|
args.reboot_if_needed,
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
versions, default_version = DISTROS[args.distro]
|
|
|
|
|
if args.version is None:
|
|
|
|
|
args.version = default_version
|
|
|
|
|
if args.version not in versions:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"Version {args.version!r} inconnue pour {args.distro}. "
|
|
|
|
|
f"Choix : {', '.join(versions)} (voir --list-images)."
|
|
|
|
|
)
|
|
|
|
|
# Certaines architectures ne sont publiées que par une partie des distros
|
|
|
|
|
# (voir ARCH_DISTRO_SUPPORT) : on valide tôt plutôt qu'échouer au download.
|
|
|
|
|
supported = ARCH_DISTRO_SUPPORT.get(args.arch)
|
|
|
|
|
if supported is not None and args.distro not in supported:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"Architecture {args.arch} indisponible pour {args.distro!r} : "
|
|
|
|
|
f"images cloud publiées seulement pour {', '.join(supported)}."
|
|
|
|
|
)
|
[ADD] catalogue: Fedora 43 for s390x
Fedora does publish an s390x cloud image, just elsewhere: the arch is
SECONDARY there, its images live under "fedora-secondary" rather than
/pub/fedora/linux/. The catalog comment read "404" -- it was the wrong
tree.
It brings what nothing else has on this architecture: qpdf 12.2.0,
exactly the pikepdf 10 threshold. The qpdf build, half an hour under
emulation, disappears. With GCC 15, Python 3.14 and cargo 1.90, none of
the s390x workarounds fire there.
Only 43 is kept: Fedora builds s390x for the current release only. 41 and
42 return 404 on the master mirror, 44 sits on third-party mirrors only.
Hence a positive per-architecture declaration, read by both interfaces --
the catalog never offers what deployment would refuse.
--- FR ---
Fedora publie bien une image cloud s390x, mais ailleurs : l'architecture
est SECONDAIRE chez elle, ses images vivent sous « fedora-secondary » et
non sous /pub/fedora/linux/. Le commentaire du catalogue disait « 404 » —
c'était la mauvaise arborescence.
Elle apporte ce qu'aucune autre n'a sur cette architecture : qpdf 12.2.0,
soit exactement le seuil de pikepdf 10. La compilation de qpdf, une demi-
heure sous émulation, disparaît. Avec GCC 15, Python 3.14 et cargo 1.90,
aucun des contournements s390x ne s'y déclenche.
Seule la 43 est retenue : Fedora ne construit s390x que pour la version
courante. Les 41 et 42 rendent 404 sur le miroir maître, la 44 n'est que
sur certains miroirs tiers. D'où une déclaration positive par
architecture, que les deux interfaces relisent — le catalogue ne propose
pas ce que le déploiement refuserait.
Assisted-by: Claude Opus 5
2026-08-11 19:55:09 -04:00
|
|
|
# Toutes les versions d'une distro ne sont pas publiées sur toutes les
|
|
|
|
|
# architectures : le dire ici plutôt que d'échouer au téléchargement.
|
|
|
|
|
ok_versions = arch_versions(args.distro, args.arch, versions)
|
|
|
|
|
if args.version not in ok_versions:
|
|
|
|
|
sys.exit(
|
|
|
|
|
f"{args.distro} {args.version} n'est pas publié en {args.arch}.\n"
|
|
|
|
|
f" Versions disponibles : {', '.join(ok_versions) or 'aucune'}."
|
|
|
|
|
)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
code, default_osinfo, min_ram, min_disk = versions[args.version]
|
|
|
|
|
if args.codename:
|
|
|
|
|
code = args.codename
|
|
|
|
|
osinfo = args.osinfo or default_osinfo
|
|
|
|
|
# Dimensionnement par défaut = minimum requis par la version (libosinfo).
|
|
|
|
|
if args.memory is None:
|
|
|
|
|
args.memory = min_ram
|
|
|
|
|
if args.disk_size is None:
|
|
|
|
|
args.disk_size = min_disk
|
|
|
|
|
# Un nombre nu (« 30 ») serait pris pour des OCTETS par qemu-img et ferait
|
|
|
|
|
# échouer le resize : on suppose des Go par défaut (« 30 » -> « 30G »).
|
|
|
|
|
if re.fullmatch(r"\d+", str(args.disk_size)):
|
|
|
|
|
args.disk_size = f"{args.disk_size}G"
|
|
|
|
|
urls = image_candidates(
|
|
|
|
|
args.distro, code, args.arch, args.version, args.dry_run
|
|
|
|
|
)
|
|
|
|
|
url = urls[0]
|
|
|
|
|
# --verify s'appuie sur un SHA256SUMS style Ubuntu ; Debian/Fedora
|
|
|
|
|
# publient des sommes dans un autre format -> on saute proprement.
|
|
|
|
|
do_verify = args.verify and args.distro == "ubuntu"
|
|
|
|
|
if args.verify and not do_verify:
|
|
|
|
|
print(
|
|
|
|
|
f" Note : --verify n'est pris en charge que pour ubuntu "
|
|
|
|
|
f"(ignoré pour {args.distro})."
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Chemin de l'image : déduit automatiquement si non fourni.
|
|
|
|
|
if args.image_path is None:
|
|
|
|
|
args.image_path = args.image_dir / default_image_name(
|
|
|
|
|
args.distro, code, args.arch, args.version
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
runner = Runner(
|
|
|
|
|
use_sudo=not args.no_sudo and os.geteuid() != 0, dry_run=args.dry_run
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# -- Mode téléchargement seul : aucun outil ni VM requis. --------------
|
|
|
|
|
if args.download_only:
|
|
|
|
|
print(
|
|
|
|
|
f"\n== Téléchargement image cloud "
|
|
|
|
|
f"({args.distro} {args.version} / {code}) =="
|
|
|
|
|
)
|
|
|
|
|
print(f" Destination : {args.image_path}")
|
|
|
|
|
download_image(urls, args.image_path, args.dry_run)
|
|
|
|
|
if do_verify:
|
|
|
|
|
verify_sha256(url, args.image_path, args.dry_run)
|
|
|
|
|
print("\nTerminé (téléchargement seul).")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# -- Déploiement complet -----------------------------------------------
|
|
|
|
|
if not args.name:
|
|
|
|
|
sys.exit(
|
|
|
|
|
"Erreur : --name est requis pour déployer une VM "
|
|
|
|
|
"(ou utilisez --download-only)."
|
|
|
|
|
)
|
|
|
|
|
args.hostname = args.hostname or args.name
|
|
|
|
|
|
|
|
|
|
pw_hash = resolve_password(args)
|
|
|
|
|
ssh_keys = load_ssh_keys(args.ssh_key)
|
|
|
|
|
if not pw_hash and not ssh_keys:
|
|
|
|
|
print(
|
|
|
|
|
"ATTENTION : ni mot de passe ni clé SSH -> connexion impossible à la VM.\n"
|
|
|
|
|
" Ajoutez --ssh-key, --ask-password ou --password-hash.\n",
|
|
|
|
|
file=sys.stderr,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
disk = args.disk_dir / f"{args.name}.qcow2"
|
|
|
|
|
seed = args.seed_dir / f"{args.name}-seed.iso"
|
|
|
|
|
|
|
|
|
|
if not args.dry_run:
|
|
|
|
|
ensure_tools(runner, args.assume_yes, args.no_install_deps)
|
|
|
|
|
# Émulation requise si l'arch diffère de l'hôte : installe l'émulateur
|
|
|
|
|
# QEMU système adéquat (+ firmware UEFI pour arm64) et prévient de la
|
|
|
|
|
# lenteur.
|
|
|
|
|
if args.arch in EMULATOR_BINARY and args.arch != host_arch():
|
|
|
|
|
ensure_emulator(
|
|
|
|
|
args.arch, runner, args.assume_yes, args.no_install_deps
|
|
|
|
|
)
|
|
|
|
|
print(
|
|
|
|
|
f" Note : {args.arch} est ÉMULÉ (TCG) sur cet hôte "
|
|
|
|
|
f"({host_arch()}) — le boot et l'installation seront "
|
|
|
|
|
"nettement plus lents que l'architecture native."
|
|
|
|
|
)
|
|
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
installer: tuple[Path, Path] | None = None
|
|
|
|
|
if uses_installer(args.distro, args.arch):
|
|
|
|
|
# Voie debian-installer : aucune image cloud n'existe pour cette
|
|
|
|
|
# combinaison, on télécharge l'installateur et on part d'un disque nu.
|
|
|
|
|
cache = args.image_path.parent
|
|
|
|
|
kernel = cache / f"debian-{args.version}-s390x-kernel"
|
|
|
|
|
initrd_src = cache / f"debian-{args.version}-s390x-initrd.gz"
|
|
|
|
|
initrd = cache / f"{args.name}-initrd.gz"
|
[FIX] todo qemu : lire les distros par arch dans deploy_qemu
L'ecran de deploiement portait sa PROPRE copie de S390X_DISTROS, sous un
commentaire promettant qu'elle restait « coherente avec deploy_qemu.py ».
Une copie ne tient aucune promesse : Debian a gagne s390x la-bas et
l'ecran ne le proposait toujours pas.
_qemu_arch_distros lit desormais ARCH_DISTRO_SUPPORT, et _qemu_arches_for
y passe aussi pour que « toutes les architectures » n'offre rien que
deploy_qemu refuserait ensuite. Les tuples locaux restent en repli si
l'import echoue. L'import est memorise : le catalogue l'interrogeait une
fois par couple (distro, version).
Plancher memoire de 2048 Mio sur la voie installateur, annonce et jamais
abaissant : d-i deplie un systeme de fichiers en RAM la ou une image
cloud arrive installee, et le manque s'y voit comme un ecran fige.
--- EN ---
The deploy screen carried its OWN copy of S390X_DISTROS, under a comment
promising it stayed "consistent with deploy_qemu.py". A copy keeps no
promise: Debian gained s390x over there and the screen still did not
offer it.
_qemu_arch_distros now reads ARCH_DISTRO_SUPPORT, and _qemu_arches_for
goes through it too, so "all architectures" offers nothing deploy_qemu
would later refuse. The local tuples remain as a fallback if the import
fails. The import is memoised: the catalogue queried it once per
(distro, version) pair.
A 2048 MiB memory floor on the installer path, announced and never
lowering: d-i unpacks a filesystem into RAM where a cloud image arrives
installed, and running short shows up as a frozen screen.
Assisted-by: Claude Opus 5
(cherry picked from commit f0ee70c1ac43ba2bc9f5fb78fe71f821a15daf7d)
2026-08-13 16:44:27 -04:00
|
|
|
# Le dimensionnement du catalogue vient des images cloud, où le
|
|
|
|
|
# système est DÉJÀ installé. debian-installer, lui, déplie un système
|
|
|
|
|
# de fichiers complet en mémoire avant d'écrire quoi que ce soit :
|
|
|
|
|
# 1024 Mio est le plancher annoncé par Debian, sans marge, et un
|
|
|
|
|
# manque de mémoire s'y manifeste par un écran figé sans message.
|
|
|
|
|
# On relève le plancher, en le disant — un réglage explicite plus haut
|
|
|
|
|
# n'est jamais abaissé.
|
|
|
|
|
if args.memory < INSTALLER_MIN_RAM:
|
|
|
|
|
print(
|
|
|
|
|
f" Mémoire portée à {INSTALLER_MIN_RAM} Mio pour"
|
|
|
|
|
f" l'installateur (catalogue : {args.memory})."
|
|
|
|
|
)
|
|
|
|
|
args.memory = INSTALLER_MIN_RAM
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
print(f"\n== 1/5 Installateur Debian {args.version} ({code}) s390x ==")
|
|
|
|
|
download_image(
|
|
|
|
|
[INSTALLER_URL.format(code=code, fichier="kernel.debian")],
|
|
|
|
|
kernel,
|
|
|
|
|
args.dry_run,
|
|
|
|
|
)
|
|
|
|
|
download_image(
|
|
|
|
|
[INSTALLER_URL.format(code=code, fichier="initrd.debian")],
|
|
|
|
|
initrd_src,
|
|
|
|
|
args.dry_run,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
print(f"\n== 2-3/5 Disque vierge {disk} ({args.disk_size}) ==")
|
|
|
|
|
create_blank_disk(disk, args.disk_size, runner, args.force)
|
|
|
|
|
|
|
|
|
|
print(f"\n== 4/5 Preseed embarqué dans l'initrd ==")
|
|
|
|
|
build_installer_initrd(
|
|
|
|
|
build_preseed(args, pw_hash, ssh_keys), initrd_src, initrd, runner
|
|
|
|
|
)
|
|
|
|
|
installer = (kernel, initrd)
|
|
|
|
|
else:
|
|
|
|
|
print(
|
|
|
|
|
f"\n== 1/5 Image cloud ({args.distro} {args.version} / {code}) =="
|
|
|
|
|
)
|
|
|
|
|
download_image(urls, args.image_path, args.dry_run)
|
|
|
|
|
if do_verify:
|
|
|
|
|
verify_sha256(url, args.image_path, args.dry_run)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
print(f"\n== 2-3/5 Disque de travail {disk} ({args.disk_size}) ==")
|
|
|
|
|
prepare_disk(args.image_path, disk, args.disk_size, runner, args.force)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
print(f"\n== 4/5 Seed cloud-init {seed} ==")
|
|
|
|
|
cloud_cfg = build_cloud_config(args, pw_hash, ssh_keys)
|
|
|
|
|
build_seed(cloud_cfg, args.hostname, seed, runner)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
resolved_osinfo = osinfo_arg(osinfo, args.distro)
|
|
|
|
|
print(f"\n== 5/5 virt-install (--osinfo {resolved_osinfo}) ==")
|
|
|
|
|
ensure_network(network_name(args.network), runner)
|
[ADD] qemu : Debian sur s390x, par debian-installer
Debian ne publie aucune image cloud s390x — verifie sur le miroir :
bookworm et trixie ne servent que amd64, arm64, ppc64el et riscv64. Le
port existe pourtant, « binary-s390x » repond 200, et l'installateur
livre kernel + initrd pour les deux versions.
D'ou une seconde voie de deploiement, choisie par uses_installer() :
disque VIERGE au lieu d'un qcow2 converti, amorcage kernel+initrd au
lieu de --import, et un preseed a la place du seed cloud-init. Le
preseed refait ce que fait cloud-init — nom d'hote, utilisateur, cles
SSH, sudo, fuseau, paquets — sinon d-i pose la question sur une console
que personne ne regarde.
Le preseed voyage DANS l'initrd : pas de serveur HTTP a maintenir
pendant l'installation, et rien qui depende du moment ou le reseau
monte.
Verifie contre l'initrd s390x reel : preseed.cfg relu a la racine des
966 entrees, 1807 octets identiques a l'ecriture. Les voies image cloud
sont inchangees — amd64, arm64 et Ubuntu s390x gardent --import.
--- EN ---
Debian publishes no s390x cloud image — checked against the mirror:
bookworm and trixie only serve amd64, arm64, ppc64el and riscv64. Yet
the port exists, "binary-s390x" returns 200, and the installer ships
kernel + initrd for both versions.
Hence a second deployment path, picked by uses_installer(): a BLANK
disk instead of a converted qcow2, kernel+initrd boot instead of
--import, and a preseed in place of the cloud-init seed. The preseed
redoes what cloud-init does — hostname, user, SSH keys, sudo, timezone,
packages — otherwise d-i asks on a console nobody watches.
The preseed travels INSIDE the initrd: no HTTP server to keep alive
during the install, and nothing depending on when the network comes up.
Verified against the real s390x initrd: preseed.cfg read back at the
root of 966 entries, 1807 bytes identical to what was written. Cloud
image paths are unchanged — amd64, arm64 and Ubuntu s390x keep --import.
Assisted-by: Claude Opus 5
(cherry picked from commit d26ddee95bb5b3b3627950c33879155fb28bba58)
2026-08-13 16:35:42 -04:00
|
|
|
virt_install(args, disk, seed, resolved_osinfo, runner, installer)
|
[ADD] qemu: deploy ERPLibre VMs from cloud images
Deploys Ubuntu, Debian, Fedora and Arch cloud images through libvirt, on
amd64, arm64 and s390x. Handles the image download with mirror fallback,
the cloud-init seed, UEFI without Secure Boot, and the host setup.
--- FR ---
Déploie des images cloud Ubuntu, Debian, Fedora et Arch via libvirt, en
amd64, arm64 et s390x. Gère le téléchargement avec repli sur miroir, le seed
cloud-init, l'UEFI sans Secure Boot et la préparation de l'hôte.
Assisted-by: Claude Opus 4.8
2026-08-07 03:22:26 -04:00
|
|
|
|
|
|
|
|
has_key = bool(ssh_keys)
|
|
|
|
|
print("\nTerminé. Suivi :")
|
|
|
|
|
print(" virsh list --all")
|
|
|
|
|
print(f" virsh console {args.name} # Ctrl+] pour quitter")
|
|
|
|
|
|
|
|
|
|
if args.dry_run:
|
|
|
|
|
print("\n Connexion SSH (IP attribuée au 1er boot) :")
|
|
|
|
|
print(f" {ssh_command(args.user, '<IP>', has_key)}")
|
|
|
|
|
return
|
|
|
|
|
if args.no_wait_ip:
|
|
|
|
|
print(
|
|
|
|
|
f"\n Récupérer l'IP : virsh domifaddr {args.name} --source lease"
|
|
|
|
|
)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
print(f"\n Attente de l'IP (bail DHCP, max {args.ip_timeout} s)…")
|
|
|
|
|
ip = wait_for_ip(args.name, runner.use_sudo, args.ip_timeout)
|
|
|
|
|
if ip:
|
|
|
|
|
print(f" IP : {ip}")
|
|
|
|
|
print(" Connexion SSH :")
|
|
|
|
|
print(f" {ssh_command(args.user, ip, has_key)}")
|
|
|
|
|
else:
|
|
|
|
|
print(" IP non obtenue dans le délai (cloud-init encore en cours ?).")
|
|
|
|
|
print(f" Réessayez : virsh domifaddr {args.name} --source lease")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|