feat: enhance Ubuntu rootfs build process with improved logging and error handling
This commit is contained in:
+107
-32
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shlex
|
||||
import shutil
|
||||
import tarfile
|
||||
import tempfile
|
||||
@@ -20,7 +21,7 @@ from .system import CommandRunner, check_kvm, ensure_data_directories, require_r
|
||||
|
||||
|
||||
_UBUNTU_RELEASE = "jammy"
|
||||
_ROOTFS_RECIPE = "uvm-ubuntu-jammy-v1\n"
|
||||
_ROOTFS_RECIPE = "uvm-ubuntu-jammy-v2\n"
|
||||
_GUEST_PACKAGES = (
|
||||
"ca-certificates", "openssh-server", "sudo", "systemd-sysv", "udev",
|
||||
"dbus", "libpam-systemd", "iproute2", "iputils-ping", "curl",
|
||||
@@ -30,6 +31,24 @@ _EXT4_FEATURES = (
|
||||
"none,has_journal,ext_attr,resize_inode,dir_index,filetype,extent,64bit,"
|
||||
"flex_bg,sparse_super,large_file,huge_file,dir_nlink,extra_isize,metadata_csum"
|
||||
)
|
||||
_ROOTFS_NAMESPACE = (
|
||||
"unshare", "--mount", "--propagation", "private", "--pid", "--fork",
|
||||
"--kill-child", "--mount-proc",
|
||||
)
|
||||
_GUEST_APT_SCRIPT = """\
|
||||
root=$1
|
||||
shift
|
||||
mount --rbind /dev "$root/dev"
|
||||
mount --make-rslave "$root/dev"
|
||||
mount -t proc proc "$root/proc"
|
||||
mount -t sysfs -o ro sysfs "$root/sys"
|
||||
chroot "$root" /usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C \\
|
||||
apt-get -o APT::Update::Error-Mode=any update
|
||||
chroot "$root" /usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C \\
|
||||
apt-get -y -o Dpkg::Options::=--force-confold dist-upgrade
|
||||
chroot "$root" /usr/bin/env DEBIAN_FRONTEND=noninteractive LC_ALL=C \\
|
||||
apt-get -y --no-install-recommends install "$@"
|
||||
"""
|
||||
|
||||
|
||||
class Installer:
|
||||
@@ -258,33 +277,36 @@ class Installer:
|
||||
with tempfile.TemporaryDirectory(prefix=".ubuntu-build-", dir=destination.parent) as name:
|
||||
temporary = Path(name)
|
||||
root = temporary / "root"
|
||||
policy = root / "usr/sbin/policy-rc.d"
|
||||
policy.parent.mkdir(parents=True)
|
||||
policy.write_text("#!/bin/sh\nexit 101\n", encoding="utf-8")
|
||||
policy.chmod(0o755)
|
||||
# Package scripts run natively, with private mounts/PIDs and service startup disabled.
|
||||
self._runner.run((
|
||||
"unshare", "--mount", "--propagation", "private", "--pid", "--fork",
|
||||
"--kill-child", "--mount-proc", "env", "DEBIAN_FRONTEND=noninteractive",
|
||||
"debootstrap", f"--arch={architecture}", "--variant=minbase", "--merged-usr",
|
||||
"--components=main,universe", f"--include={','.join(_GUEST_PACKAGES)}",
|
||||
f"--extra-suites={_UBUNTU_RELEASE}-updates,{_UBUNTU_RELEASE}-security",
|
||||
"--force-check-gpg", "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg",
|
||||
_UBUNTU_RELEASE, root, mirror,
|
||||
))
|
||||
policy.unlink()
|
||||
self._configure_ubuntu_rootfs(root, mirror)
|
||||
self._validate_ubuntu_rootfs(root)
|
||||
build_log = temporary / "build.log"
|
||||
try:
|
||||
# Bootstrap one consistent release; apt resolves updated package versions later.
|
||||
self._run_rootfs_stage("Ubuntu bootstrap", (
|
||||
*_ROOTFS_NAMESPACE, "env", "DEBIAN_FRONTEND=noninteractive",
|
||||
"debootstrap", f"--arch={architecture}", "--variant=minbase", "--merged-usr",
|
||||
"--components=main", "--include=ca-certificates,ubuntu-keyring",
|
||||
"--force-check-gpg", "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg",
|
||||
_UBUNTU_RELEASE, root, mirror,
|
||||
), build_log)
|
||||
self._install_ubuntu_packages(root, mirror, build_log)
|
||||
self._configure_ubuntu_rootfs(root)
|
||||
self._validate_ubuntu_rootfs(root)
|
||||
|
||||
disk = temporary / "rootfs.ext4"
|
||||
with disk.open("wb") as image:
|
||||
image.truncate(2 * 1024 * 1024 * 1024)
|
||||
disk.chmod(0o600)
|
||||
self._runner.run((
|
||||
"mkfs.ext4", "-q", "-F", "-b", "4096", "-O", _EXT4_FEATURES,
|
||||
"-E", "lazy_itable_init=0,lazy_journal_init=0", "-d", root, disk,
|
||||
), capture=True)
|
||||
self._runner.run(("e2fsck", "-f", "-n", disk), capture=True)
|
||||
disk = temporary / "rootfs.ext4"
|
||||
with disk.open("wb") as image:
|
||||
image.truncate(2 * 1024 * 1024 * 1024)
|
||||
disk.chmod(0o600)
|
||||
self._run_rootfs_stage("Ubuntu ext4 formatting", (
|
||||
"mkfs.ext4", "-q", "-F", "-b", "4096", "-O", _EXT4_FEATURES,
|
||||
"-E", "lazy_itable_init=0,lazy_journal_init=0", "-d", root, disk,
|
||||
), build_log)
|
||||
self._run_rootfs_stage("Ubuntu filesystem check", ("e2fsck", "-f", "-n", disk), build_log)
|
||||
except (UvmError, OSError, UnicodeError) as error:
|
||||
try:
|
||||
log = self._save_rootfs_build_log(destination, root, build_log, error=error)
|
||||
except OSError as log_error:
|
||||
raise UvmError(f"Ubuntu rootfs build failed: {error}; could not save diagnostics: {log_error}") from error
|
||||
raise UvmError(f"Ubuntu rootfs build failed: {error}; details saved to {log}") from error
|
||||
self._save_rootfs_build_log(destination, root, build_log)
|
||||
with disk.open("rb") as image:
|
||||
os.fsync(image.fileno())
|
||||
os.replace(disk, destination)
|
||||
@@ -296,12 +318,65 @@ class Installer:
|
||||
except (OSError, UnicodeError) as error:
|
||||
raise UvmError(f"could not build Ubuntu guest rootfs: {error}") from error
|
||||
|
||||
def _configure_ubuntu_rootfs(self, root: Path, mirror: str) -> None:
|
||||
def _install_ubuntu_packages(self, root: Path, mirror: str, build_log: Path) -> None:
|
||||
sources = root / "etc/apt/sources.list"
|
||||
sources.write_text("".join(
|
||||
f"deb {mirror} {suite} main universe\n"
|
||||
for suite in (_UBUNTU_RELEASE, f"{_UBUNTU_RELEASE}-updates", f"{_UBUNTU_RELEASE}-security")
|
||||
), encoding="utf-8")
|
||||
sources.chmod(0o644)
|
||||
# Ubuntu's debootstrap script removes its own policy-rc.d on success.
|
||||
policy = root / "usr/sbin/policy-rc.d"
|
||||
policy.write_text("#!/bin/sh\nexit 101\n", encoding="utf-8")
|
||||
policy.chmod(0o755)
|
||||
try:
|
||||
self._run_rootfs_stage("Ubuntu guest package installation", (
|
||||
*_ROOTFS_NAMESPACE, "/bin/sh", "-eu", "-c", _GUEST_APT_SCRIPT,
|
||||
"uvm-guest-packages", root, *_GUEST_PACKAGES,
|
||||
), build_log)
|
||||
finally:
|
||||
policy.unlink(missing_ok=True)
|
||||
|
||||
def _run_rootfs_stage(self, stage: str, command: tuple[str | Path, ...], log: Path) -> None:
|
||||
if self._emit is not None:
|
||||
self._emit(stage)
|
||||
with log.open("a", encoding="utf-8") as output:
|
||||
output.write(f"\n=== {stage} ===\n+ {shlex.join(map(str, command))}\n")
|
||||
result = self._runner.run(command, check=False, capture=True)
|
||||
output.write(result.stdout)
|
||||
output.write(result.stderr)
|
||||
if result.returncode != 0:
|
||||
raise UvmError(f"{stage} failed (exit {result.returncode})")
|
||||
|
||||
@staticmethod
|
||||
def _save_rootfs_build_log(destination: Path, root: Path, build_log: Path, *, error: Exception | None = None) -> Path:
|
||||
log = destination.with_name("ubuntu-build.log")
|
||||
descriptor, name = tempfile.mkstemp(prefix=".ubuntu-log-", dir=destination.parent)
|
||||
temporary = Path(name)
|
||||
try:
|
||||
with os.fdopen(descriptor, "wb") as output:
|
||||
# Completed bootstrap details precede apt output; failed bootstrap details follow it.
|
||||
for source in (root / "var/log/bootstrap.log", build_log, root / "debootstrap/debootstrap.log"):
|
||||
if source.is_file():
|
||||
output.write(f"\n=== {source.name} ===\n".encode("utf-8"))
|
||||
with source.open("rb") as content:
|
||||
shutil.copyfileobj(content, output)
|
||||
if error is not None:
|
||||
output.write(f"\nUbuntu rootfs build failed: {error}\n".encode("utf-8", errors="replace"))
|
||||
output.flush()
|
||||
os.fsync(output.fileno())
|
||||
os.replace(temporary, log)
|
||||
directory = os.open(log.parent, os.O_RDONLY)
|
||||
try:
|
||||
os.fsync(directory)
|
||||
finally:
|
||||
os.close(directory)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
return log
|
||||
|
||||
def _configure_ubuntu_rootfs(self, root: Path) -> None:
|
||||
files = {
|
||||
"etc/apt/sources.list": "".join(
|
||||
f"deb {mirror} {suite} main universe\n"
|
||||
for suite in (_UBUNTU_RELEASE, f"{_UBUNTU_RELEASE}-updates", f"{_UBUNTU_RELEASE}-security")
|
||||
),
|
||||
"etc/fstab": "/dev/vda / ext4 defaults 0 1\n",
|
||||
"etc/hostname": "uvm\n",
|
||||
"etc/hosts": "127.0.0.1 localhost\n127.0.1.1 uvm\n",
|
||||
|
||||
Reference in New Issue
Block a user