]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add vmspawn verb
authorSam Leonard <sam.leonard@codethink.co.uk>
Tue, 31 Oct 2023 15:43:32 +0000 (15:43 +0000)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 22 Feb 2024 12:16:12 +0000 (13:16 +0100)
mkosi/__init__.py
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi.md
mkosi/vmspawn.py [new file with mode: 0644]
tests/__init__.py
tests/test_boot.py
tests/test_config.py

index fcb320ce3174d3540b880eeac295e14464b39eb6..08b1913ea7b113a3326323715d4c748f2f5f3cb1 100644 (file)
@@ -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)
index 3d747191cda48d0ea5374dfcfc3eed25ae460870..b11bd8ca96dc31597a075aabd62ec91c561b08bf 100644 (file)
@@ -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...]
index 3ff905956ab2cf4d917c54b6e13c437cc5190561..c0350032201e7dd3bfde76a8a6d2224d5d714f83 100644 (file)
@@ -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
index 6e4bcab473ebdb47d28d3f9dc0252bba3c73836b..c238c9edcd667dab197f58fefe48542fc51f2e42 100644 (file)
@@ -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 (file)
index 0000000..102012e
--- /dev/null
@@ -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)
index ffe28d5c62a06abdb771c9cbce93afb263e96612..26aca3bb8a526a0c0e6b211f34d4703d9710fec9 100644 (file)
@@ -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)
 
index 66f0027792c4113c1d9ffde7fee4b94746b9b6f4..0ec42b7c70241aa679b816990dd2c3bc40e311e4 100644 (file)
@@ -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
 
index a06c0e37f21a51ede24c859aa2ca3aafd6c6acd4..3eb0dd815543877c8da1a010d7dc43ce6ddaac2e 100644 (file)
@@ -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"])