[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)
This commit is contained in:
parent
0da6c7fcbe
commit
9641a5b749
1 changed files with 240 additions and 14 deletions
|
|
@ -48,6 +48,7 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import getpass
|
||||
import grp
|
||||
import gzip
|
||||
import hashlib
|
||||
import os
|
||||
import re
|
||||
|
|
@ -173,6 +174,13 @@ S390X_DISTROS: tuple[str, ...] = (
|
|||
"rocky",
|
||||
"fedora",
|
||||
"opensuse",
|
||||
# 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",
|
||||
)
|
||||
|
||||
# Une distro peut ne publier qu'une PARTIE de ses versions sur une
|
||||
|
|
@ -184,9 +192,29 @@ S390X_DISTROS: tuple[str, ...] = (
|
|||
# (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, ...]]] = {
|
||||
"s390x": {"fedora": ("43",)},
|
||||
# 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")},
|
||||
}
|
||||
|
||||
# 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"),)
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def arch_versions(distro: str, arch: str, versions) -> list[str]:
|
||||
"""Versions de `distro` réellement publiées pour `arch`."""
|
||||
|
|
@ -1679,6 +1707,141 @@ def prepare_disk(
|
|||
runner.run(["qemu-img", "resize", str(disk), size], privileged=True)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
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(","):
|
||||
|
|
@ -1749,6 +1912,7 @@ def virt_install(
|
|||
seed: Path,
|
||||
osinfo: str,
|
||||
runner: Runner,
|
||||
installer: tuple[Path, Path] | None = None,
|
||||
) -> None:
|
||||
# Émulée (TCG, pas de KVM) si l'arch demandée diffère de celle de l'hôte.
|
||||
# Deux causes d'émulation, à ne pas confondre : une architecture étrangère
|
||||
|
|
@ -1817,17 +1981,41 @@ def virt_install(
|
|||
str(args.memory),
|
||||
"--vcpus",
|
||||
str(args.vcpus),
|
||||
"--import",
|
||||
]
|
||||
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 += [
|
||||
"--disk",
|
||||
f"path={disk},format=qcow2,bus=virtio",
|
||||
]
|
||||
# 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:
|
||||
# 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.
|
||||
"--disk",
|
||||
f"path={seed},readonly=on,bus=virtio",
|
||||
cmd += ["--disk", f"path={seed},readonly=on,bus=virtio"]
|
||||
cmd += [
|
||||
"--osinfo",
|
||||
osinfo,
|
||||
"--network",
|
||||
|
|
@ -1881,6 +2069,13 @@ def virt_install(
|
|||
cmd += ["--virt-type", "qemu"]
|
||||
if not args.attach_console:
|
||||
cmd.append("--noautoconsole")
|
||||
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"]
|
||||
# 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
|
||||
|
|
@ -2382,22 +2577,53 @@ def main() -> None:
|
|||
"nettement plus lents que l'architecture native."
|
||||
)
|
||||
|
||||
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)
|
||||
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"
|
||||
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 de travail {disk} ({args.disk_size}) ==")
|
||||
prepare_disk(args.image_path, disk, args.disk_size, runner, args.force)
|
||||
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 Seed cloud-init {seed} ==")
|
||||
cloud_cfg = build_cloud_config(args, pw_hash, ssh_keys)
|
||||
build_seed(cloud_cfg, args.hostname, seed, runner)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
resolved_osinfo = osinfo_arg(osinfo, args.distro)
|
||||
print(f"\n== 5/5 virt-install (--osinfo {resolved_osinfo}) ==")
|
||||
ensure_network(network_name(args.network), runner)
|
||||
virt_install(args, disk, seed, resolved_osinfo, runner)
|
||||
virt_install(args, disk, seed, resolved_osinfo, runner, installer)
|
||||
|
||||
has_key = bool(ssh_keys)
|
||||
print("\nTerminé. Suivi :")
|
||||
|
|
|
|||
Loading…
Reference in a new issue