umask,
)
from mkosi.versioncomp import GenericVersion
+from mkosi.vmspawn import run_vmspawn
MKOSI_AS_CALLER = (
"setpriv",
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:
Verb.qemu: run_qemu,
Verb.serve: run_serve,
Verb.burn: run_burn,
+ Verb.vmspawn: run_vmspawn,
}[args.verb](args, last)
journalctl = enum.auto()
coredumpctl = enum.auto()
burn = enum.auto()
+ vmspawn = enum.auto()
def supports_cmdline(self) -> bool:
return self in (
Verb.journalctl,
Verb.coredumpctl,
Verb.burn,
+ Verb.vmspawn,
)
def needs_build(self) -> bool:
Verb.qemu,
Verb.serve,
Verb.burn,
+ Verb.vmspawn,
)
def needs_root(self) -> bool:
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:
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...]
)
+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,
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
`mkosi [options…] qemu [qemu parameters…]`
+`mkosi [options…] vmspawn [vmspawn settings…]`
+
`mkosi [options…] ssh [command line…]`
`mkosi [options…] journalctl [command line…]`
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
--- /dev/null
+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)
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)
# 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(
image.qemu(options=options)
+ if have_vmspawn() and format in (OutputFormat.disk, OutputFormat.directory):
+ image.vmspawn(options=options)
+
if format != OutputFormat.disk:
return
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"])