From: Sam Leonard Date: Tue, 31 Oct 2023 15:43:32 +0000 (+0000) Subject: Add vmspawn verb X-Git-Tag: v21~36 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5aa968771b4a616db6bb8fab382a57a4dc1ef47e;p=thirdparty%2Fmkosi.git Add vmspawn verb --- diff --git a/mkosi/__init__.py b/mkosi/__init__.py index fcb320ce3..08b1913ea 100644 --- a/mkosi/__init__.py +++ b/mkosi/__init__.py @@ -80,6 +80,7 @@ from mkosi.util import ( umask, ) from mkosi.versioncomp import GenericVersion +from mkosi.vmspawn import run_vmspawn MKOSI_AS_CALLER = ( "setpriv", @@ -2281,6 +2282,9 @@ def check_tools(config: Config, verb: Verb) -> None: if verb == Verb.boot: check_systemd_tool(config, "systemd-nspawn", version="254", reason="boot images") + if verb == Verb.vmspawn: + check_systemd_tool(config, "systemd-vmspawn", version="256", reason="boot images with vmspawn") + def configure_ssh(context: Context) -> None: if not context.config.ssh: @@ -3796,4 +3800,5 @@ def run_verb(args: Args, images: Sequence[Config], *, resources: Path) -> None: Verb.qemu: run_qemu, Verb.serve: run_serve, Verb.burn: run_burn, + Verb.vmspawn: run_vmspawn, }[args.verb](args, last) diff --git a/mkosi/config.py b/mkosi/config.py index 3d747191c..b11bd8ca9 100644 --- a/mkosi/config.py +++ b/mkosi/config.py @@ -70,6 +70,7 @@ class Verb(StrEnum): journalctl = enum.auto() coredumpctl = enum.auto() burn = enum.auto() + vmspawn = enum.auto() def supports_cmdline(self) -> bool: return self in ( @@ -81,6 +82,7 @@ class Verb(StrEnum): Verb.journalctl, Verb.coredumpctl, Verb.burn, + Verb.vmspawn, ) def needs_build(self) -> bool: @@ -91,6 +93,7 @@ class Verb(StrEnum): Verb.qemu, Verb.serve, Verb.burn, + Verb.vmspawn, ) def needs_root(self) -> bool: @@ -105,6 +108,13 @@ class ConfigFeature(StrEnum): enabled = enum.auto() disabled = enum.auto() + def to_tristate(self) -> str: + if self == ConfigFeature.enabled: + return "yes" + if self == ConfigFeature.disabled: + return "no" + return "" + @dataclasses.dataclass(frozen=True) class ConfigTree: @@ -2637,6 +2647,7 @@ def create_argument_parser(action: type[argparse.Action]) -> argparse.ArgumentPa mkosi [options...] {b}shell{e} [command line...] mkosi [options...] {b}boot{e} [nspawn settings...] mkosi [options...] {b}qemu{e} [qemu parameters...] + mkosi [options...] {b}vmspawn{e} [vmspawn parameters...] mkosi [options...] {b}ssh{e} [command line...] mkosi [options...] {b}journalctl{e} [command line...] mkosi [options...] {b}coredumpctl{e} [command line...] diff --git a/mkosi/qemu.py b/mkosi/qemu.py index 3ff905956..c03500322 100644 --- a/mkosi/qemu.py +++ b/mkosi/qemu.py @@ -478,6 +478,25 @@ def want_scratch(config: Config) -> bool: ) +def finalize_qemu_firmware(config: Config, kernel: Optional[Path]) -> QemuFirmware: + if config.qemu_firmware == QemuFirmware.auto: + if kernel: + return ( + QemuFirmware.uefi + if KernelType.identify(config, kernel) != KernelType.unknown + else QemuFirmware.linux + ) + elif ( + config.output_format in (OutputFormat.cpio, OutputFormat.directory) or + config.architecture.to_efi() is None + ): + return QemuFirmware.linux + else: + return QemuFirmware.uefi + else: + return config.qemu_firmware + + def run_qemu(args: Args, config: Config) -> None: if config.output_format not in ( OutputFormat.disk, @@ -534,22 +553,7 @@ def run_qemu(args: Args, config: Config) -> None: if kernel and not kernel.exists(): die(f"Kernel not found at {kernel}") - if config.qemu_firmware == QemuFirmware.auto: - if kernel: - firmware = ( - QemuFirmware.uefi - if KernelType.identify(config, kernel) != KernelType.unknown - else QemuFirmware.linux - ) - elif ( - config.output_format in (OutputFormat.cpio, OutputFormat.directory) or - config.architecture.to_efi() is None - ): - firmware = QemuFirmware.linux - else: - firmware = QemuFirmware.uefi - else: - firmware = config.qemu_firmware + firmware = finalize_qemu_firmware(config, kernel) if ( not kernel and diff --git a/mkosi/resources/mkosi.md b/mkosi/resources/mkosi.md index 6e4bcab47..c238c9edc 100644 --- a/mkosi/resources/mkosi.md +++ b/mkosi/resources/mkosi.md @@ -18,6 +18,8 @@ mkosi — Build Bespoke OS Images `mkosi [options…] qemu [qemu parameters…]` +`mkosi [options…] vmspawn [vmspawn settings…]` + `mkosi [options…] ssh [command line…]` `mkosi [options…] journalctl [command line…]` @@ -89,6 +91,14 @@ The following command line verbs are known: the `qemu` verb. Any arguments specified after the `qemu` verb are appended to the `qemu` invocation. +`vmspawn` + +: Similar to `boot`, but uses `systemd-vmspawn` to boot up the image, i.e. + instead of container virtualization virtual machine virtualization is used. + This verb is only supported for disk and directory type images. + Any arguments specified after the `vmspawn` verb are appended to the + `systemd-vmspawn` invocation. + `ssh` : When the image is built with the `Ssh=yes` option, this command diff --git a/mkosi/vmspawn.py b/mkosi/vmspawn.py new file mode 100644 index 000000000..102012e18 --- /dev/null +++ b/mkosi/vmspawn.py @@ -0,0 +1,102 @@ +import contextlib +import os +import sys +from pathlib import Path + +from mkosi.config import ( + Args, + Config, + OutputFormat, + QemuFirmware, + yes_no, +) +from mkosi.log import die +from mkosi.qemu import ( + copy_ephemeral, + finalize_qemu_firmware, +) +from mkosi.run import run +from mkosi.types import PathString + + +def run_vmspawn(args: Args, config: Config) -> None: + if config.output_format not in (OutputFormat.disk, OutputFormat.directory): + die(f"{config.output_format} images cannot be booted in systemd-vmspawn") + + if config.qemu_firmware == QemuFirmware.bios: + die("systemd-vmspawn cannot boot BIOS firmware images") + + if config.qemu_cdrom: + die("systemd-vmspawn does not support CD-ROM images") + + kernel = config.qemu_kernel + + if kernel and not kernel.exists(): + die(f"Kernel not found at {kernel}") + + firmware = finalize_qemu_firmware(config, kernel) + + if not kernel and firmware == QemuFirmware.linux: + kernel = config.output_dir_or_cwd() / config.output_split_kernel + if not kernel.exists(): + die( + f"Kernel or UKI not found at {kernel}", + hint="Please install a kernel in the image or provide a --qemu-kernel argument to mkosi vmspawn" + ) + + cmdline: list[PathString] = [ + "systemd-vmspawn", + "--qemu-smp", config.qemu_smp, + "--qemu-mem", config.qemu_mem, + "--qemu-kvm", config.qemu_kvm.to_tristate(), + "--qemu-vsock", config.qemu_vsock.to_tristate(), + "--tpm", config.qemu_swtpm.to_tristate(), + "--secure-boot", yes_no(config.secure_boot), + ] + + if config.qemu_gui: + cmdline += ["--qemu-gui"] + + cmdline += [f"--set-credential={k}:{v}" for k, v in config.credentials.items()] + + with contextlib.ExitStack() as stack: + if config.ephemeral: + fname = stack.enter_context(copy_ephemeral(config, config.output_dir_or_cwd() / config.output)) + else: + fname = config.output_dir_or_cwd() / config.output + + if config.output_format == OutputFormat.disk and config.runtime_size: + run( + [ + "systemd-repart", + "--definitions", "", + "--no-pager", + f"--size={config.runtime_size}", + "--pretty=no", + "--offline=yes", + fname, + ], + sandbox=config.sandbox(options=["--bind", fname, fname]), + ) + + kcl = config.kernel_command_line_extra + + for tree in config.runtime_trees: + target = Path("/root/src") / (tree.target or tree.source.name) + cmdline += ["--bind", f"{tree.source}:{target}"] + + if kernel: + cmdline += ["--linux", kernel] + + if config.output_format == OutputFormat.directory: + cmdline += ["--directory", fname] + + owner = os.stat(fname).st_uid + if owner != 0: + cmdline += [f"--private-users={str(owner)}"] + else: + cmdline += ["--image", fname] + + cmdline += [*args.cmdline, *kcl] + + run(cmdline, stdin=sys.stdin, stdout=sys.stdout, env=os.environ, log=False) diff --git a/tests/__init__.py b/tests/__init__.py index ffe28d5c6..26aca3bb8 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -124,6 +124,22 @@ class Image: return result + def vmspawn(self, options: Sequence[str] = (), args: Sequence[str] = ()) -> CompletedProcess: + result = self.mkosi( + "vmspawn", + [*options, "--debug"], + args, + stdin=sys.stdin if sys.stdin.isatty() else None, + check=False, + ) + + rc = 0 if self.config.distribution.is_centos_variant() else 123 + + if result.returncode != rc: + raise subprocess.CalledProcessError(result.returncode, result.args, result.stdout, result.stderr) + + return result + def summary(self, options: Sequence[str] = ()) -> CompletedProcess: return self.mkosi("summary", options, user=INVOKING_USER.uid, group=INVOKING_USER.gid) diff --git a/tests/test_boot.py b/tests/test_boot.py index 66f002779..0ec42b7c7 100644 --- a/tests/test_boot.py +++ b/tests/test_boot.py @@ -1,18 +1,29 @@ # SPDX-License-Identifier: LGPL-2.1+ import os +import subprocess import pytest from mkosi.config import OutputFormat from mkosi.distributions import Distribution from mkosi.qemu import find_virtiofsd +from mkosi.run import find_binary, run +from mkosi.versioncomp import GenericVersion from . import Image pytestmark = pytest.mark.integration +def have_vmspawn() -> bool: + return ( + find_binary("systemd-vmspawn") is not None + and GenericVersion(run(["systemd-vmspawn", "--version"], + stdout=subprocess.PIPE).stdout.strip()) >= 256 + ) + + @pytest.mark.parametrize("format", OutputFormat) def test_boot(config: Image.Config, format: OutputFormat) -> None: with Image( @@ -58,6 +69,9 @@ def test_boot(config: Image.Config, format: OutputFormat) -> None: image.qemu(options=options) + if have_vmspawn() and format in (OutputFormat.disk, OutputFormat.directory): + image.vmspawn(options=options) + if format != OutputFormat.disk: return diff --git a/tests/test_config.py b/tests/test_config.py index a06c0e37f..3eb0dd815 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -318,6 +318,7 @@ def test_parse_load_verb(tmp_path: Path) -> None: assert parse_config(["qemu"])[0].verb == Verb.qemu assert parse_config(["journalctl"])[0].verb == Verb.journalctl assert parse_config(["coredumpctl"])[0].verb == Verb.coredumpctl + assert parse_config(["vmspawn"])[0].verb == Verb.vmspawn with pytest.raises(SystemExit): parse_config(["invalid"])