Dev #4

Merged
kstyagi merged 9 commits from dev into main 2026-09-23 02:53:58 +00:00
5 changed files with 273 additions and 62 deletions
Showing only changes of commit bd92573158 - Show all commits
+21 -5
View File
@@ -211,7 +211,8 @@ Default runtime storage is `/var/lib/uvm`:
│ └── jailer
├── images/
│ ├── vmlinux
│ └── ubuntu.ext4
│ ├── ubuntu.ext4
│ └── ubuntu-build.log
├── vms/
│ └── vm-<uuid>/
│ ├── rootfs.ext4
@@ -713,10 +714,17 @@ Explicit kernel/rootfs URLs and trusted digests must match the host architecture
uses an uncompressed Linux `Image` (PE format) and an ARM64 ext4 rootfs.
The default rootfs builder installs `debootstrap` and `ubuntu-keyring`, uses
`--force-check-gpg`, and includes main/universe plus updates/security suites.
`--force-check-gpg`, and bootstraps only the base release's main component with
CA certificates and the Ubuntu keyring. Do not pass updates/security as
`--extra-suites`: debootstrap can download base archives but import newer
pre-dependency metadata, producing an empty `dpkg --install` invocation.
After bootstrap, apt sees main/universe plus updates/security, runs update
with `APT::Update::Error-Mode=any`, upgrades the base, and installs guest packages.
amd64 uses `archive.ubuntu.com/ubuntu`; arm64 uses `ports.ubuntu.com/ubuntu-ports`.
Package scripts execute in private mount/PID namespaces with a temporary
`policy-rc.d` suppressing service startup. No mounts survive the build process.
Both phases execute in private mount/PID namespaces. The apt phase has private
dev/proc and read-only sys mounts, and recreates `policy-rc.d` to suppress service
startup after Ubuntu's debootstrap script removes it. Cleanup tolerates an
already-removed policy file. No mounts survive the build process.
The staged tree retains `/var/lib/dpkg` and apt metadata, validates installed
apt/sudo/sshd/systemd packages, enables SSH/networkd, preserves kernel-provided
static networking, and supplies guest DNS. Template host keys and machine IDs
@@ -729,6 +737,11 @@ permit reuse of a built template. Old demo images and unverified caches are
rebuilt, and `install --force` always rebuilds. A failed staging build retains
the old template. Installation holds the existing operation lock throughout;
existing VM disks are never upgraded or reprovisioned by installation.
Stage command output and debootstrap's detailed log are atomically saved to
`images/ubuntu-build.log` (mode `0600`) on success or failure before deleting
staging files. Errors name that durable log; the last lines contain the latest
stage's diagnostics and failure summary. The recipe marker is bumped whenever
the build workflow changes so an earlier cached template cannot skip it.
The host package list includes `nftables` and `conntrack` as well as the
existing iproute2/iptables, ext4, OpenSSL, OpenSSH, download, and util-linux
@@ -924,7 +937,10 @@ growth, shrinking, geometry, and retained data/password-hash/host-key fixtures.
UID/GID allocation, account/home metadata, existing accounts, and password hashes.
`test_install.py` simulates package bootstrap and exercises real ext4 formatting
and readback for retained dpkg metadata, sudo modes, template configuration,
staging failures, and checksum provenance. It does not run debootstrap or boot a VM.
staging failures, durable build diagnostics, and checksum provenance. The apt
shell script runs against stub mount/chroot commands to check argument handling
and stop-on-error behavior. Tests simulate Ubuntu removing `policy-rc.d` after
bootstrap. They do not run debootstrap or boot a VM.
Lifecycle resize tests use fake processes/networking to verify ordering,
single-lock ownership, identity preservation, pending intent, and failure recovery.
+9 -2
View File
@@ -70,8 +70,10 @@ sudo env \
```
The Ubuntu build uses `debootstrap`, authenticated Ubuntu archive signatures,
and native packages. UVM installs the builder prerequisites and records the
built ext4 image's SHA-256 in the integrity manifest. Building requires root
and native packages. It bootstraps the base release first, then uses apt to
apply updates/security fixes and install the guest packages. UVM installs the
builder prerequisites and records the built ext4 image's SHA-256 in the
integrity manifest. Building requires root
with mount/PID namespace support and internet access to Ubuntu mirrors.
Use `--force` to refresh downloads and rebuild the Ubuntu template:
@@ -95,6 +97,11 @@ Existing VM disks retain their original OS and credentials; installation
updates the template used for future VMs. Restart a running API server to load
the updated provisioner before creating VMs through the dashboard.
If the build fails, its detailed bootstrap/package output is retained in
`/var/lib/uvm/images/ubuntu-build.log` (mode `0600`). Correct the build failure
and rerun installation before creating a custom-user VM; a failed build leaves
the previous template installed.
For local experimentation only, the old unverified quick-start behavior is
available through an explicit opt-out:
+16 -3
View File
@@ -175,9 +175,10 @@ Installation prints the Firecracker, kernel, and rootfs paths under
`/var/lib/uvm/integrity.json`.
The default build installs `debootstrap` and `ubuntu-keyring` on the host,
checks Ubuntu archive signatures, and builds inside private mount/PID
namespaces. It requires native root execution with namespace support. Leave
`UVM_ROOTFS_URL` and `UVM_ROOTFS_SHA256` unset for this mode. To use a prebuilt
checks Ubuntu archive signatures, bootstraps the base release, then uses apt
to apply updates and install the guest packages. It builds inside private
mount/PID namespaces and requires native root execution with namespace support.
Leave `UVM_ROOTFS_URL` and `UVM_ROOTFS_SHA256` unset for this mode. To use a prebuilt
ext4 image, supply both its URL and SHA-256 instead.
### Scenario: Refresh Firecracker or Guest Assets
@@ -227,6 +228,18 @@ sudo env UVM_ALLOW_UNVERIFIED_DOWNLOADS=1 ./uvm.py start VM
| `SHA-256 mismatch` | Stop and obtain the correct digest or artifact URL. |
| `unsupported host architecture` | Use an x86_64 or ARM64 Linux host. |
| `unshare: Operation not permitted` | Run the builder as root on a host permitting private mount/PID namespaces. |
| `Ubuntu rootfs build failed` | Read `images/ubuntu-build.log` under `UVM_BASE`, fix the reported bootstrap/apt error, and rerun installation. |
The latest image-build log is retained after temporary build directories are
cleaned up, including detailed debootstrap and apt output:
```sh
sudo tail -n 100 /var/lib/uvm/images/ubuntu-build.log
```
A failed build leaves the previous rootfs template installed. If creation then
reports missing sudo, complete the image installation successfully before
retrying creation. The username itself does not cause that error.
## 5. Create VMs
+120 -20
View File
@@ -2,8 +2,10 @@ from __future__ import annotations
import hashlib
import io
import json
import os
import shutil
import sys
import tarfile
import tempfile
import unittest
@@ -13,9 +15,9 @@ from types import SimpleNamespace
from unittest.mock import Mock, patch
from uvm.config import Settings
from uvm.errors import CommandError, UvmError
from uvm.errors import UvmError
from uvm.images import ImageStore
from uvm.install import Installer, _ROOTFS_RECIPE
from uvm.install import Installer, _GUEST_APT_SCRIPT, _ROOTFS_RECIPE
from uvm.integrity import load_manifest, write_manifest
from uvm.state import StateStore
from uvm.system import CommandResult, CommandRunner
@@ -154,7 +156,7 @@ class InstallerArchitectureTests(unittest.TestCase):
def test_only_verified_current_recipe_images_are_reused(self) -> None:
for verified, marker, force in (
(True, _ROOTFS_RECIPE, False), (False, _ROOTFS_RECIPE, False),
(True, "", False), (True, _ROOTFS_RECIPE, True),
(True, "", False), (True, "uvm-ubuntu-jammy-v1\n", False), (True, _ROOTFS_RECIPE, True),
):
with self.subTest(verified=verified, marker=marker, force=force), tempfile.TemporaryDirectory() as temporary:
settings = Settings(base=Path(temporary))
@@ -184,35 +186,108 @@ class UbuntuBuildRunner:
if "debootstrap" in args:
root = Path(args[-2])
if self.failure == "bootstrap":
raise CommandError("bootstrap interrupted")
self.write_files(root, {"debootstrap/debootstrap.log": "dpkg: --install needs at least one package archive file argument\n"})
return CommandResult(args, 1, stderr="W: Failure while unpacking the base system\n")
self.write_files(root, {
"etc/passwd": "root:x:0:0::/root:/bin/bash\n",
"etc/shadow": "root:*:1:0:99999:7:::\n", "etc/group": "root:x:0:\n",
"etc/apt/sources.list": "deb https://ports.ubuntu.com/ubuntu-ports jammy main\n",
"usr/bin/apt-get": "apt binary",
"var/lib/dpkg/status": "Package: apt\nStatus: install ok installed\nVersion: 1.0\n",
"var/lib/dpkg/info/apt.list": "/usr/bin/apt-get\n",
"var/log/bootstrap.log": "Base system installed successfully.\n",
})
for relative in ("dev", "proc", "sys", "usr/sbin"):
(root / relative).mkdir(parents=True, exist_ok=True)
(root / "etc/resolv.conf").symlink_to("../run/systemd/resolve/stub-resolv.conf")
# Ubuntu's real script removes this, even if it existed before bootstrap.
(root / "usr/sbin/policy-rc.d").unlink(missing_ok=True)
return CommandResult(args, 0, stdout="I: Base system installed successfully.\n")
if "uvm-guest-packages" in args:
root = Path(args[args.index("uvm-guest-packages") + 1])
if (root / "usr/sbin/policy-rc.d").read_text() != "#!/bin/sh\nexit 101\n":
raise AssertionError("guest services must be disabled during apt provisioning")
sources = (root / "etc/apt/sources.list").read_text()
if "jammy-updates main universe" not in sources or "jammy-security main universe" not in sources:
raise AssertionError("apt must see updates and security before installing guest packages")
if self.failure == "packages":
return CommandResult(args, 100, stdout="Reading package lists...\n", stderr="E: unmet guest dependencies\n")
status = "\n\n".join(
f"Package: {package}\nStatus: install ok installed\nVersion: 1.0"
for package in ("apt", "sudo", "openssh-server", "systemd-sysv")
) + "\n"
files = {
"etc/passwd": "root:x:0:0::/root:/bin/bash\n",
"etc/shadow": "root:*:1:0:99999:7:::\n", "etc/group": "root:x:0:\n",
"etc/sudoers": "root ALL=(ALL:ALL) ALL\n",
"etc/ssh/sshd_config": "UsePAM yes\n",
"etc/ssh/ssh_host_rsa_key": "template host key",
"usr/bin/sudo": "sudo binary", "usr/bin/apt-get": "apt binary",
"usr/bin/sudo": "sudo binary",
"usr/sbin/sshd": "sshd binary", "usr/lib/systemd/systemd": "init binary",
"var/lib/dpkg/status": status,
"var/lib/dpkg/info/apt.list": "/usr/bin/apt-get\n",
"var/lib/dpkg/info/sudo.list": "/usr/bin/sudo\n",
}
if self.failure == "metadata":
files["var/lib/dpkg/status"] = ""
self.write_files(root, files)
(root / "usr/bin/sudo").chmod(0o4755)
return CommandResult(args, 0, stdout="Configured guest packages.\n")
if args[0] == "systemctl":
return CommandResult(args, 0)
return self.runner.run(command, **options)
@staticmethod
def write_files(root: Path, files: dict[str, str]) -> None:
for relative, contents in files.items():
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(contents, encoding="utf-8")
(root / "usr/bin/sudo").chmod(0o4755)
(root / "etc/resolv.conf").symlink_to("../run/systemd/resolve/stub-resolv.conf")
return CommandResult(args, 0)
if args[0] == "systemctl":
return CommandResult(args, 0)
return self.runner.run(command, **options)
class UbuntuAptStageTests(unittest.TestCase):
def test_apt_script_preserves_arguments_and_stops_after_an_update_failure(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
base = Path(temporary)
root = base / "guest with spaces"
policy = root / "usr/sbin/policy-rc.d"
policy.parent.mkdir(parents=True)
policy.write_text("#!/bin/sh\nexit 101\n", encoding="utf-8")
tools = base / "tools"
tools.mkdir()
for name in ("mount", "chroot"):
tool = tools / name
tool.write_text(
f"#!{sys.executable}\n"
"import json, os, sys\nfrom pathlib import Path\n"
"name = Path(sys.argv[0]).name\n"
"print(json.dumps([name, *sys.argv[1:]]))\n"
"if name == 'chroot':\n"
" assert (Path(sys.argv[1]) / 'usr/sbin/policy-rc.d').is_file()\n"
" if os.environ.get('FAIL_UPDATE') == '1' and sys.argv[-1] == 'update':\n"
" sys.exit(100)\n",
encoding="utf-8",
)
tool.chmod(0o755)
runner = CommandRunner(emit=None)
for fail in (False, True):
with self.subTest(fail=fail):
result = runner.run((
"env", f"PATH={tools}", f"FAIL_UPDATE={int(fail)}",
"/bin/sh", "-eu", "-c", _GUEST_APT_SCRIPT,
"uvm-guest-packages", root, "sudo", "openssh-server",
), check=False, capture=True)
events = [json.loads(line) for line in result.stdout.splitlines()]
mounts = [event for event in events if event[0] == "mount"]
self.assertEqual([event[-1] for event in mounts], [
str(root / "dev"), str(root / "dev"), str(root / "proc"), str(root / "sys"),
])
calls = [event for event in events if event[0] == "chroot"]
self.assertEqual(result.returncode, 100 if fail else 0)
self.assertEqual(len(calls), 1 if fail else 3)
self.assertEqual(calls[0][-3:], ["-o", "APT::Update::Error-Mode=any", "update"])
for call in calls:
self.assertEqual(call[1:6], [str(root), "/usr/bin/env", "DEBIAN_FRONTEND=noninteractive", "LC_ALL=C", "apt-get"])
if not fail:
self.assertEqual(calls[1][-1], "dist-upgrade")
self.assertEqual(calls[2][-3:], ["install", "sudo", "openssh-server"])
@unittest.skipUnless(all(shutil.which(tool) for tool in ("mkfs.ext4", "e2fsck", "debugfs")),
@@ -233,11 +308,23 @@ class UbuntuRootfsBuildTests(unittest.TestCase):
self.assertIn(f"--arch={deb_arch}", bootstrap)
self.assertEqual(bootstrap[-1], mirror)
self.assertIn("--force-check-gpg", bootstrap)
self.assertIn("--extra-suites=jammy-updates,jammy-security", bootstrap)
self.assertIn("--components=main", bootstrap)
self.assertIn("--include=ca-certificates,ubuntu-keyring", bootstrap)
self.assertFalse(any(part.startswith("--extra-suites") for part in bootstrap))
self.assertIn("--mount-proc", bootstrap)
self.assertIn("--kill-child", bootstrap)
packages = next(call for call in runner.calls if "uvm-guest-packages" in call)
self.assertLess(runner.calls.index(bootstrap), runner.calls.index(packages))
self.assertIn("sudo", packages)
self.assertIn("openssh-server", packages)
self.assertIn("--mount-proc", packages)
self.assertIn("--kill-child", packages)
self.assertEqual(settings.rootfs_image.stat().st_mode & 0o777, 0o600)
self.assertEqual(list(settings.images_dir.iterdir()), [settings.rootfs_image])
log = settings.images_dir / "ubuntu-build.log"
self.assertEqual(set(settings.images_dir.iterdir()), {settings.rootfs_image, log})
self.assertEqual(log.stat().st_mode & 0o777, 0o600)
self.assertIn("Base system installed successfully.", log.read_text())
self.assertIn("Configured guest packages.", log.read_text())
def guest(command):
return runner.runner.run(("debugfs", "-R", command, settings.rootfs_image), capture=True)
@@ -252,16 +339,29 @@ class UbuntuRootfsBuildTests(unittest.TestCase):
for path in ("/usr/sbin/policy-rc.d", "/etc/ssh/ssh_host_rsa_key"):
self.assertIn("File not found", guest(f"stat {path}").stderr)
def test_failed_bootstrap_or_missing_package_metadata_preserves_the_old_template(self) -> None:
for failure in ("bootstrap", "metadata"):
def test_failed_build_preserves_the_old_template_and_retains_diagnostics(self) -> None:
for failure in ("bootstrap", "packages", "metadata"):
with self.subTest(failure=failure), tempfile.TemporaryDirectory() as temporary:
settings = Settings(base=Path(temporary))
settings.images_dir.mkdir()
settings.rootfs_image.write_bytes(b"old template")
runner = UbuntuBuildRunner(failure=failure)
installer = Installer(settings, runner, StateStore(settings), emit=None) # type: ignore[arg-type]
with self.assertRaises(UvmError):
log = settings.images_dir / "ubuntu-build.log"
with self.assertRaisesRegex(UvmError, "details saved to .*ubuntu-build.log"):
installer._build_ubuntu_rootfs(settings.rootfs_image)
self.assertEqual(settings.rootfs_image.read_bytes(), b"old template")
self.assertEqual(list(settings.images_dir.iterdir()), [settings.rootfs_image])
self.assertEqual(set(settings.images_dir.iterdir()), {settings.rootfs_image, log})
self.assertEqual(log.stat().st_mode & 0o777, 0o600)
details = log.read_text()
if failure == "bootstrap":
self.assertIn("--install needs at least one package archive", details)
self.assertIn("Failure while unpacking", details)
self.assertFalse(any("uvm-guest-packages" in call for call in runner.calls))
elif failure == "packages":
self.assertIn("Reading package lists...", details)
self.assertIn("E: unmet guest dependencies", details)
self.assertLess(details.index("Base system installed successfully."), details.index("E: unmet guest dependencies"))
else:
self.assertIn("incomplete apt/dpkg package metadata", details)
self.assertFalse(any(call[0] == "mkfs.ext4" for call in runner.calls))
+96 -21
View File
@@ -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",
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,universe", f"--include={','.join(_GUEST_PACKAGES)}",
f"--extra-suites={_UBUNTU_RELEASE}-updates,{_UBUNTU_RELEASE}-security",
"--components=main", "--include=ca-certificates,ubuntu-keyring",
"--force-check-gpg", "--keyring=/usr/share/keyrings/ubuntu-archive-keyring.gpg",
_UBUNTU_RELEASE, root, mirror,
))
policy.unlink()
self._configure_ubuntu_rootfs(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((
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,
), capture=True)
self._runner.run(("e2fsck", "-f", "-n", disk), capture=True)
), 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:
files = {
"etc/apt/sources.list": "".join(
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/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",