Files
nest/tests/test_install.py
T
kstyagi 4bf4c0853c Enhance UVM installation and configuration for Ubuntu 22.04 root filesystem
- Update README and USAGE documentation to reflect changes in rootfs handling, including the default local build from Ubuntu 22.04 packages.
- Modify the installer to support building the rootfs locally with authenticated packages and to require SHA-256 checksums for prebuilt images.
- Implement checks for setuid permissions on sudo and update the provisioning logic for guest users to ensure proper sudo access.
- Adjust tests to validate the new rootfs building process and ensure that existing functionality is preserved.
- Refactor code to improve clarity and maintainability, including the introduction of constants for Ubuntu release and guest packages.
2026-09-22 00:33:05 +05:30

268 lines
14 KiB
Python

from __future__ import annotations
import hashlib
import io
import os
import shutil
import tarfile
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock, patch
from uvm.config import Settings
from uvm.errors import CommandError, UvmError
from uvm.images import ImageStore
from uvm.install import Installer, _ROOTFS_RECIPE
from uvm.integrity import load_manifest, write_manifest
from uvm.state import StateStore
from uvm.system import CommandResult, CommandRunner
class InstallerArchitectureTests(unittest.TestCase):
def test_installs_matching_native_artifacts_with_a_verified_manifest(self) -> None:
for machine, architecture in (
("x86_64", "x86_64"),
("aarch64", "aarch64"),
("arm64", "aarch64"),
):
with (
self.subTest(machine=machine),
tempfile.TemporaryDirectory() as temporary_directory,
patch.dict(os.environ, {"UVM_BASE": temporary_directory}, clear=True),
patch("uvm.config.os.uname", return_value=SimpleNamespace(machine=machine)),
):
settings = Settings.from_environment()
version = settings.firecracker_version
binaries = {
"firecracker": f"firecracker for {architecture}".encode(),
"jailer": f"jailer for {architecture}".encode(),
}
archive_buffer = io.BytesIO()
with tarfile.open(fileobj=archive_buffer, mode="w:gz") as archive:
for name, content in binaries.items():
member = tarfile.TarInfo(
f"release-{version}-{architecture}/{name}-{version}-{architecture}"
)
member.size = len(content)
member.mode = 0o755
archive.addfile(member, io.BytesIO(content))
archive_bytes = archive_buffer.getvalue()
kernel_bytes = f"kernel for {architecture}".encode()
rootfs_bytes = f"rootfs for {architecture}".encode()
settings = replace(
settings,
firecracker_sha256=hashlib.sha256(archive_bytes).hexdigest(),
kernel_sha256=hashlib.sha256(kernel_bytes).hexdigest(),
rootfs_sha256=hashlib.sha256(rootfs_bytes).hexdigest(),
rootfs_url=(
"https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/"
f"{architecture}/rootfs/bionic.rootfs.ext4"
),
)
guest_base = (
"https://s3.amazonaws.com/spec.ccfc.min/img/quickstart_guide/"
f"{architecture}"
)
payloads = {
"https://github.com/firecracker-microvm/firecracker/releases/download/"
f"{version}/firecracker-{version}-{architecture}.tgz": archive_bytes,
f"{guest_base}/kernels/vmlinux.bin": kernel_bytes,
f"{guest_base}/rootfs/bionic.rootfs.ext4": rootfs_bytes,
}
runner = Mock(spec=CommandRunner)
installer = Installer(settings, runner, StateStore(settings), emit=None)
with (
patch("uvm.install.require_root"),
patch("uvm.install.check_kvm"),
patch(
"uvm.install.urllib.request.urlopen",
side_effect=lambda url, timeout: io.BytesIO(payloads[url]),
) as download,
):
firecracker, assets = installer.install()
self.assertEqual(firecracker.read_bytes(), binaries["firecracker"])
self.assertEqual(settings.jailer_binary.read_bytes(), binaries["jailer"])
self.assertEqual(assets.kernel.read_bytes(), kernel_bytes)
self.assertEqual(assets.rootfs.read_bytes(), rootfs_bytes)
self.assertEqual(download.call_count, 3)
self.assertEqual({call.args[0] for call in download.call_args_list}, set(payloads))
manifest = load_manifest(settings.integrity_manifest_path)
self.assertTrue(manifest.verified)
self.assertEqual(manifest.checksums, {
name: hashlib.sha256(content).hexdigest()
for name, content in {
**binaries, "kernel": kernel_bytes, "rootfs": rootfs_bytes,
}.items()
})
def test_arm64_install_requires_checksums_before_host_commands_or_downloads(self) -> None:
with tempfile.TemporaryDirectory() as temporary_directory:
settings = Settings(base=Path(temporary_directory), architecture="aarch64")
runner = Mock(spec=CommandRunner)
installer = Installer(settings, runner, StateStore(settings), emit=None)
with (
patch("uvm.install.require_root"),
patch("uvm.install.check_kvm") as check_kvm,
patch("uvm.install.urllib.request.urlopen") as download,
self.assertRaisesRegex(UvmError, "Firecracker archive checksum is required"),
):
installer.install()
runner.run.assert_not_called()
check_kvm.assert_not_called()
download.assert_not_called()
self.assertFalse(settings.integrity_manifest_path.exists())
def test_default_build_records_the_authenticated_local_image_digest(self) -> None:
with tempfile.TemporaryDirectory() as temporary:
settings = Settings(
base=Path(temporary), firecracker_sha256="1" * 64,
kernel_sha256=hashlib.sha256(b"kernel").hexdigest(),
)
settings.bin_dir.mkdir()
settings.firecracker_binary.write_bytes(b"firecracker")
settings.jailer_binary.write_bytes(b"jailer")
installer = Installer(settings, Mock(spec=CommandRunner), StateStore(settings), emit=None)
with (
patch("uvm.install.require_root"), patch("uvm.install.check_kvm"),
patch.object(installer, "install_apt_packages"),
patch.object(installer, "install_firecracker_binary", return_value=settings.firecracker_binary),
patch.object(installer, "download", side_effect=lambda _url, path, **_kwargs: path.write_bytes(b"kernel")),
patch.object(installer, "_build_ubuntu_rootfs", side_effect=lambda path: path.write_bytes(b"full Ubuntu")) as build,
):
installer.install()
build.assert_called_once_with(settings.rootfs_image)
manifest = load_manifest(settings.integrity_manifest_path)
self.assertTrue(manifest.verified)
self.assertEqual(manifest.checksums["rootfs"], hashlib.sha256(b"full Ubuntu").hexdigest())
self.assertEqual(ImageStore(settings).installed_assets().rootfs, settings.rootfs_image)
def test_old_rootfs_checksum_requires_an_explicit_prebuilt_url_before_mutations(self) -> None:
settings = Settings(firecracker_sha256="1" * 64, kernel_sha256="2" * 64, rootfs_sha256="3" * 64)
runner = Mock(spec=CommandRunner)
installer = Installer(settings, runner, Mock(spec=StateStore), emit=None)
with self.assertRaisesRegex(UvmError, "unset UVM_ROOTFS_SHA256"):
installer._validate_integrity_policy()
runner.run.assert_not_called()
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),
):
with self.subTest(verified=verified, marker=marker, force=force), tempfile.TemporaryDirectory() as temporary:
settings = Settings(base=Path(temporary))
settings.images_dir.mkdir()
settings.rootfs_image.write_bytes(b"image")
write_manifest(settings.integrity_manifest_path,
{"rootfs": hashlib.sha256(b"image").hexdigest()}, verified=verified)
runner = Mock(spec=CommandRunner)
runner.run.return_value = CommandResult(("debugfs",), 0, stdout=marker)
installer = Installer(settings, runner, StateStore(settings), emit=None)
with patch.object(installer, "_build_ubuntu_rootfs") as build:
installer._install_ubuntu_rootfs(settings.rootfs_image, force=force)
self.assertEqual(build.called, not (verified and marker == _ROOTFS_RECIPE and not force))
class UbuntuBuildRunner:
"""Simulate package installation; exercise real ext4 formatting and readback."""
def __init__(self, *, failure: str | None = None) -> None:
self.calls: list[tuple[str, ...]] = []
self.failure = failure
self.runner = CommandRunner(emit=None)
def run(self, command, **options) -> CommandResult:
args = tuple(map(str, command))
self.calls.append(args)
if "debootstrap" in args:
root = Path(args[-2])
if self.failure == "bootstrap":
raise CommandError("bootstrap interrupted")
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/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"] = ""
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)
@unittest.skipUnless(all(shutil.which(tool) for tool in ("mkfs.ext4", "e2fsck", "debugfs")),
"e2fsprogs is required for offline Ubuntu image checks")
class UbuntuRootfsBuildTests(unittest.TestCase):
def test_build_keeps_package_metadata_and_sudo_permissions_with_native_signed_bootstrap(self) -> None:
for architecture, deb_arch, mirror in (
("aarch64", "arm64", "https://ports.ubuntu.com/ubuntu-ports"),
("x86_64", "amd64", "https://archive.ubuntu.com/ubuntu"),
):
with self.subTest(architecture=architecture), tempfile.TemporaryDirectory() as temporary:
settings = Settings(base=Path(temporary), architecture=architecture)
runner = UbuntuBuildRunner()
installer = Installer(settings, runner, StateStore(settings), emit=None) # type: ignore[arg-type]
installer._build_ubuntu_rootfs(settings.rootfs_image)
bootstrap = next(call for call in runner.calls if "debootstrap" in call)
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("--mount-proc", bootstrap)
self.assertIn("--kill-child", bootstrap)
self.assertEqual(settings.rootfs_image.stat().st_mode & 0o777, 0o600)
self.assertEqual(list(settings.images_dir.iterdir()), [settings.rootfs_image])
def guest(command):
return runner.runner.run(("debugfs", "-R", command, settings.rootfs_image), capture=True)
self.assertIn("Package: apt\nStatus: install ok installed", guest("cat /var/lib/dpkg/status").stdout)
self.assertEqual(guest("cat /var/lib/dpkg/info/sudo.list").stdout, "/usr/bin/sudo\n")
self.assertRegex(guest("stat /usr/bin/sudo").stdout, r"Mode:\s+0?4755")
self.assertEqual(guest("cat /etc/uvm-image").stdout, _ROOTFS_RECIPE)
self.assertIn("nameserver 1.1.1.1", guest("cat /etc/resolv.conf").stdout)
self.assertIn("KeepConfiguration=static", guest("cat /etc/systemd/network/10-uvm.network").stdout)
self.assertIn("jammy-security main universe", guest("cat /etc/apt/sources.list").stdout)
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"):
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):
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.assertFalse(any(call[0] == "mkfs.ext4" for call in runner.calls))