]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Move qemu logic into qemu.py
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Thu, 11 May 2023 13:18:38 +0000 (15:18 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Fri, 12 May 2023 05:42:00 +0000 (07:42 +0200)
We also introduce a new function qemu_check_vsock_support()

mkosi/__init__.py
mkosi/qemu.py [new file with mode: 0644]
mkosi/util.py

index d64be4e68f8c55a60db0f4746d84091ef20ac4bc..2239de8292d6fa58e6ee83ce4b75e179d1cf36cb 100644 (file)
@@ -1,7 +1,5 @@
 # SPDX-License-Identifier: LGPL-2.1+
 
-import asyncio
-import base64
 import contextlib
 import datetime
 import errno
@@ -14,7 +12,6 @@ import os
 import re
 import resource
 import shutil
-import socket
 import subprocess
 import sys
 import tempfile
@@ -37,20 +34,13 @@ from mkosi.log import Style, color_error, complete_step, die, log_step
 from mkosi.manifest import Manifest
 from mkosi.mounts import dissect_and_mount, mount_overlay, scandir_recursive
 from mkosi.pager import page
+from mkosi.qemu import machine_cid, run_qemu
 from mkosi.remove import unlink_try_hard
-from mkosi.run import (
-    MkosiAsyncioThread,
-    become_root,
-    fork_and_wait,
-    run,
-    run_workspace_command,
-    spawn,
-)
+from mkosi.run import become_root, fork_and_wait, run, run_workspace_command, spawn
 from mkosi.state import MkosiState
 from mkosi.types import PathString
 from mkosi.util import (
     Compression,
-    Distribution,
     InvokingUser,
     ManifestFormat,
     OutputFormat,
@@ -59,8 +49,6 @@ from mkosi.util import (
     format_rlimit,
     is_apt_distribution,
     prepend_to_environ_path,
-    qemu_check_kvm_support,
-    tmp_dir,
 )
 
 MKOSI_COMMANDS_NEED_BUILD = (Verb.shell, Verb.boot, Verb.qemu, Verb.serve)
@@ -1827,12 +1815,6 @@ def check_root() -> None:
         die("Must be invoked as root.")
 
 
-def machine_cid(config: MkosiConfig) -> int:
-    cid = int.from_bytes(hashlib.sha256(config.output_with_version.encode()).digest()[:4], byteorder='little')
-    # Make sure we don't return any of the well-known CIDs.
-    return max(3, min(cid, 0xFFFFFFFF - 1))
-
-
 def nspawn_knows_arg(arg: str) -> bool:
     # Specify some extra incompatible options so nspawn doesn't try to boot a container in the current
     # directory if it has a compatible layout.
@@ -1906,282 +1888,6 @@ def run_shell(args: MkosiArgs, config: MkosiConfig) -> None:
         run(cmdline, stdin=sys.stdin, stdout=sys.stdout, env=os.environ, log=False)
 
 
-def find_qemu_binary(config: MkosiConfig) -> str:
-    binaries = ["qemu", "qemu-kvm", f"qemu-system-{config.architecture}"]
-    for binary in binaries:
-        if shutil.which(binary) is not None:
-            return binary
-
-    die("Couldn't find QEMU/KVM binary")
-
-
-def find_qemu_firmware(config: MkosiConfig) -> tuple[Path, bool]:
-    FIRMWARE_LOCATIONS = {
-        "x86_64": ["/usr/share/ovmf/x64/OVMF_CODE.secboot.fd"],
-        "i386": [
-            "/usr/share/edk2/ovmf-ia32/OVMF_CODE.secboot.fd",
-            "/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd"
-        ],
-    }.get(config.architecture, [])
-
-    for firmware in FIRMWARE_LOCATIONS:
-        if os.path.exists(firmware):
-            return Path(firmware), True
-
-    FIRMWARE_LOCATIONS = {
-        "x86_64": [
-            "/usr/share/ovmf/ovmf_code_x64.bin",
-            "/usr/share/ovmf/x64/OVMF_CODE.fd",
-            "/usr/share/qemu/ovmf-x86_64.bin",
-        ],
-        "i386": ["/usr/share/ovmf/ovmf_code_ia32.bin", "/usr/share/edk2/ovmf-ia32/OVMF_CODE.fd"],
-        "aarch64": ["/usr/share/AAVMF/AAVMF_CODE.fd"],
-        "armhfp": ["/usr/share/AAVMF/AAVMF32_CODE.fd"],
-    }.get(config.architecture, [])
-
-    for firmware in FIRMWARE_LOCATIONS:
-        if os.path.exists(firmware):
-            logging.warning("Couldn't find OVMF firmware blob with secure boot support, "
-                            "falling back to OVMF firmware blobs without secure boot support.")
-            return Path(firmware), False
-
-    # If we can't find an architecture specific path, fall back to some generic paths that might also work.
-
-    FIRMWARE_LOCATIONS = [
-        "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd",
-        "/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd",  # GENTOO:
-        "/usr/share/qemu/OVMF_CODE.secboot.fd",
-        "/usr/share/ovmf/OVMF.secboot.fd",
-        "/usr/share/OVMF/OVMF_CODE.secboot.fd",
-    ]
-
-    for firmware in FIRMWARE_LOCATIONS:
-        if os.path.exists(firmware):
-            return Path(firmware), True
-
-    FIRMWARE_LOCATIONS = [
-        "/usr/share/edk2/ovmf/OVMF_CODE.fd",
-        "/usr/share/edk2-ovmf/OVMF_CODE.fd",  # GENTOO:
-        "/usr/share/qemu/OVMF_CODE.fd",
-        "/usr/share/ovmf/OVMF.fd",
-        "/usr/share/OVMF/OVMF_CODE.fd",
-    ]
-
-    for firmware in FIRMWARE_LOCATIONS:
-        if os.path.exists(firmware):
-            logging.warn("Couldn't find OVMF firmware blob with secure boot support, "
-                         "falling back to OVMF firmware blobs without secure boot support.")
-            return Path(firmware), False
-
-    die("Couldn't find OVMF UEFI firmware blob.")
-
-
-def find_ovmf_vars(config: MkosiConfig) -> Path:
-    OVMF_VARS_LOCATIONS = []
-
-    if config.architecture == "x86_64":
-        OVMF_VARS_LOCATIONS += ["/usr/share/ovmf/x64/OVMF_VARS.fd"]
-    elif config.architecture == "i386":
-        OVMF_VARS_LOCATIONS += [
-            "/usr/share/edk2/ovmf-ia32/OVMF_VARS.fd",
-            "/usr/share/OVMF/OVMF32_VARS_4M.fd",
-        ]
-    elif config.architecture == "armhfp":
-        OVMF_VARS_LOCATIONS += ["/usr/share/AAVMF/AAVMF32_VARS.fd"]
-    elif config.architecture == "aarch64":
-        OVMF_VARS_LOCATIONS += ["/usr/share/AAVMF/AAVMF_VARS.fd"]
-
-    OVMF_VARS_LOCATIONS += ["/usr/share/edk2/ovmf/OVMF_VARS.fd",
-                            "/usr/share/edk2-ovmf/OVMF_VARS.fd",  # GENTOO:
-                            "/usr/share/qemu/OVMF_VARS.fd",
-                            "/usr/share/ovmf/OVMF_VARS.fd",
-                            "/usr/share/OVMF/OVMF_VARS.fd"]
-
-    for location in OVMF_VARS_LOCATIONS:
-        if os.path.exists(location):
-            return Path(location)
-
-    die("Couldn't find OVMF UEFI variables file.")
-
-
-@contextlib.contextmanager
-def start_swtpm() -> Iterator[Optional[Path]]:
-
-    if not shutil.which("swtpm"):
-        yield None
-        return
-
-    with tempfile.TemporaryDirectory() as swtpm_state:
-        swtpm_sock = Path(swtpm_state) / Path("sock")
-
-        cmd = ["swtpm",
-               "socket",
-               "--tpm2",
-               "--tpmstate", f"dir={swtpm_state}",
-               "--ctrl", f"type=unixio,path={swtpm_sock}",
-         ]
-
-        swtpm_proc = spawn(cmd)
-
-        try:
-            yield swtpm_sock
-        finally:
-            swtpm_proc.wait()
-
-
-@contextlib.contextmanager
-def vsock_notify_handler() -> Iterator[tuple[str, dict[str, str]]]:
-    """
-    This yields a vsock address and a dict that will be filled in with the notifications from the VM. The
-    dict should only be accessed after the context manager has been finalized.
-    """
-    with socket.socket(socket.AF_VSOCK, socket.SOCK_SEQPACKET) as vsock:
-        vsock.bind((socket.VMADDR_CID_ANY, -1))
-        vsock.listen()
-        vsock.setblocking(False)
-
-        messages = {}
-
-        async def notify() -> None:
-            loop = asyncio.get_running_loop()
-
-            try:
-                while True:
-                    s, _ = await loop.sock_accept(vsock)
-
-                    for msg in (await loop.sock_recv(s, 4096)).decode().split("\n"):
-                        if not msg:
-                            continue
-
-                        k, _, v = msg.partition("=")
-                        messages[k] = v
-
-            except asyncio.CancelledError:
-                pass
-
-        with MkosiAsyncioThread(notify()):
-            yield f"vsock:{socket.VMADDR_CID_HOST}:{vsock.getsockname()[1]}", messages
-
-
-def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None:
-    accel = "tcg"
-    if config.qemu_kvm == ConfigFeature.enabled or (config.qemu_kvm == ConfigFeature.auto and qemu_check_kvm_support()):
-        accel = "kvm"
-
-    firmware, fw_supports_sb = find_qemu_firmware(config)
-    smm = "on" if fw_supports_sb else "off"
-
-    if config.architecture == "aarch64":
-        machine = f"type=virt,accel={accel}"
-    else:
-        machine = f"type=q35,accel={accel},smm={smm}"
-
-    cmdline: list[PathString] = [
-        find_qemu_binary(config),
-        "-machine", machine,
-        "-smp", config.qemu_smp,
-        "-m", config.qemu_mem,
-        "-object", "rng-random,filename=/dev/urandom,id=rng0",
-        "-device", "virtio-rng-pci,rng=rng0,id=rng-device0",
-        "-nic", "user,model=virtio-net-pci",
-    ]
-
-    try:
-        os.open("/dev/vhost-vsock", os.O_RDWR|os.O_CLOEXEC)
-        cmdline += ["-device", f"vhost-vsock-pci,guest-cid={machine_cid(config)}"]
-    except OSError as e:
-        if e.errno == errno.ENOENT:
-            logging.warning("/dev/vhost-vsock not found. Not adding a vsock device to the virtual machine.")
-        elif e.errno in (errno.EPERM, errno.EACCES):
-            logging.warning("Permission denied to access /dev/vhost-vsock. Not adding a vsock device to the virtual machine.")
-
-    cmdline += ["-cpu", "max"]
-
-    if config.qemu_gui:
-        cmdline += ["-vga", "virtio"]
-    else:
-        # -nodefaults removes the default CDROM device which avoids an error message during boot
-        # -serial mon:stdio adds back the serial device removed by -nodefaults.
-        cmdline += [
-            "-nographic",
-            "-nodefaults",
-            "-chardev", "stdio,mux=on,id=console,signal=off",
-            "-serial", "chardev:console",
-            "-mon", "console",
-        ]
-
-    for k, v in config.credentials.items():
-        cmdline += ["-smbios", f"type=11,value=io.systemd.credential.binary:{k}={base64.b64encode(v.encode()).decode()}"]
-    cmdline += ["-smbios", f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(config.kernel_command_line_extra)}"]
-
-    cmdline += ["-drive", f"if=pflash,format=raw,readonly=on,file={firmware}"]
-
-    notifications: dict[str, str] = {}
-
-    with contextlib.ExitStack() as stack:
-        if fw_supports_sb:
-            ovmf_vars = stack.enter_context(tempfile.NamedTemporaryFile(prefix=".mkosi-", dir=tmp_dir()))
-            copy_path(find_ovmf_vars(config), Path(ovmf_vars.name))
-            cmdline += [
-                "-global", "ICH9-LPC.disable_s3=1",
-                "-global", "driver=cfi.pflash01,property=secure,value=on",
-                "-drive", f"file={ovmf_vars.name},if=pflash,format=raw",
-            ]
-
-        if config.ephemeral:
-            f = stack.enter_context(tempfile.NamedTemporaryFile(prefix=".mkosi-", dir=config.output_dir))
-            fname = Path(f.name)
-
-            # So on one hand we want CoW off, since this stuff will
-            # have a lot of random write accesses. On the other we
-            # want the copy to be snappy, hence we do want CoW. Let's
-            # ask for both, and let the kernel figure things out:
-            # let's turn off CoW on the file, but start with a CoW
-            # copy. On btrfs that works: the initial copy is made as
-            # CoW but later changes do not result in CoW anymore.
-
-            run(["chattr", "+C", fname], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
-            copy_path(config.output_dir / config.output, fname)
-        else:
-            fname = config.output_dir / config.output
-
-        # Debian images fail to boot with virtio-scsi, see: https://github.com/systemd/mkosi/issues/725
-        if config.output_format == OutputFormat.cpio:
-            kernel = config.output_dir / config.output_split_kernel
-            if not kernel.exists() and "-kernel" not in args.cmdline:
-                die("No kernel found, please install a kernel in the cpio or provide a -kernel argument to mkosi qemu")
-            cmdline += ["-kernel", kernel,
-                        "-initrd", fname,
-                        "-append", " ".join(config.kernel_command_line + config.kernel_command_line_extra)]
-        if config.distribution == Distribution.debian:
-            cmdline += ["-drive", f"if=virtio,id=hd,file={fname},format=raw"]
-        else:
-            cmdline += ["-drive", f"if=none,id=hd,file={fname},format=raw",
-                        "-device", "virtio-scsi-pci,id=scsi",
-                        "-device", "scsi-hd,drive=hd,bootindex=1"]
-
-        swtpm_socket = stack.enter_context(start_swtpm())
-        if swtpm_socket is not None:
-            cmdline += ["-chardev", f"socket,id=chrtpm,path={swtpm_socket}",
-                        "-tpmdev", "emulator,id=tpm0,chardev=chrtpm"]
-
-            if config.architecture == "x86_64":
-                cmdline += ["-device", "tpm-tis,tpmdev=tpm0"]
-            elif config.architecture == "aarch64":
-                cmdline += ["-device", "tpm-tis-device,tpmdev=tpm0"]
-
-        addr, notifications = stack.enter_context(vsock_notify_handler())
-        cmdline += ["-smbios", f"type=11,value=io.systemd.credential:vmm.notify_socket={addr}"]
-
-        cmdline += config.qemu_args
-        cmdline += args.cmdline
-
-        run(cmdline, stdin=sys.stdin, stdout=sys.stdout, env=os.environ, log=False)
-
-    if "EXIT_STATUS" in notifications:
-        raise subprocess.CalledProcessError(int(notifications["EXIT_STATUS"]), cmdline)
-
-
 def run_ssh(args: MkosiArgs, config: MkosiConfig) -> None:
     cmd = [
         "ssh",
diff --git a/mkosi/qemu.py b/mkosi/qemu.py
new file mode 100644 (file)
index 0000000..c7e5afa
--- /dev/null
@@ -0,0 +1,304 @@
+# SPDX-License-Identifier: LGPL-2.1+
+
+import asyncio
+import base64
+import contextlib
+import hashlib
+import logging
+import os
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+from pathlib import Path
+from typing import Iterator, Optional
+
+from mkosi.config import ConfigFeature, MkosiArgs, MkosiConfig
+from mkosi.install import copy_path
+from mkosi.log import die
+from mkosi.run import MkosiAsyncioThread, run, spawn
+from mkosi.types import PathString
+from mkosi.util import (
+    Distribution,
+    OutputFormat,
+    qemu_check_kvm_support,
+    qemu_check_vsock_support,
+    tmp_dir,
+)
+
+
+def machine_cid(config: MkosiConfig) -> int:
+    cid = int.from_bytes(hashlib.sha256(config.output_with_version.encode()).digest()[:4], byteorder='little')
+    # Make sure we don't return any of the well-known CIDs.
+    return max(3, min(cid, 0xFFFFFFFF - 1))
+
+
+def find_qemu_binary(config: MkosiConfig) -> str:
+    binaries = ["qemu", "qemu-kvm", f"qemu-system-{config.architecture}"]
+    for binary in binaries:
+        if shutil.which(binary) is not None:
+            return binary
+
+    die("Couldn't find QEMU/KVM binary")
+
+
+def find_qemu_firmware(config: MkosiConfig) -> tuple[Path, bool]:
+    FIRMWARE_LOCATIONS = {
+        "x86_64": ["/usr/share/ovmf/x64/OVMF_CODE.secboot.fd"],
+        "i386": [
+            "/usr/share/edk2/ovmf-ia32/OVMF_CODE.secboot.fd",
+            "/usr/share/OVMF/OVMF32_CODE_4M.secboot.fd"
+        ],
+    }.get(config.architecture, [])
+
+    for firmware in FIRMWARE_LOCATIONS:
+        if os.path.exists(firmware):
+            return Path(firmware), True
+
+    FIRMWARE_LOCATIONS = {
+        "x86_64": [
+            "/usr/share/ovmf/ovmf_code_x64.bin",
+            "/usr/share/ovmf/x64/OVMF_CODE.fd",
+            "/usr/share/qemu/ovmf-x86_64.bin",
+        ],
+        "i386": ["/usr/share/ovmf/ovmf_code_ia32.bin", "/usr/share/edk2/ovmf-ia32/OVMF_CODE.fd"],
+        "aarch64": ["/usr/share/AAVMF/AAVMF_CODE.fd"],
+        "armhfp": ["/usr/share/AAVMF/AAVMF32_CODE.fd"],
+    }.get(config.architecture, [])
+
+    for firmware in FIRMWARE_LOCATIONS:
+        if os.path.exists(firmware):
+            logging.warning("Couldn't find OVMF firmware blob with secure boot support, "
+                            "falling back to OVMF firmware blobs without secure boot support.")
+            return Path(firmware), False
+
+    # If we can't find an architecture specific path, fall back to some generic paths that might also work.
+
+    FIRMWARE_LOCATIONS = [
+        "/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd",
+        "/usr/share/edk2-ovmf/OVMF_CODE.secboot.fd",  # GENTOO:
+        "/usr/share/qemu/OVMF_CODE.secboot.fd",
+        "/usr/share/ovmf/OVMF.secboot.fd",
+        "/usr/share/OVMF/OVMF_CODE.secboot.fd",
+    ]
+
+    for firmware in FIRMWARE_LOCATIONS:
+        if os.path.exists(firmware):
+            return Path(firmware), True
+
+    FIRMWARE_LOCATIONS = [
+        "/usr/share/edk2/ovmf/OVMF_CODE.fd",
+        "/usr/share/edk2-ovmf/OVMF_CODE.fd",  # GENTOO:
+        "/usr/share/qemu/OVMF_CODE.fd",
+        "/usr/share/ovmf/OVMF.fd",
+        "/usr/share/OVMF/OVMF_CODE.fd",
+    ]
+
+    for firmware in FIRMWARE_LOCATIONS:
+        if os.path.exists(firmware):
+            logging.warn("Couldn't find OVMF firmware blob with secure boot support, "
+                         "falling back to OVMF firmware blobs without secure boot support.")
+            return Path(firmware), False
+
+    die("Couldn't find OVMF UEFI firmware blob.")
+
+
+def find_ovmf_vars(config: MkosiConfig) -> Path:
+    OVMF_VARS_LOCATIONS = []
+
+    if config.architecture == "x86_64":
+        OVMF_VARS_LOCATIONS += ["/usr/share/ovmf/x64/OVMF_VARS.fd"]
+    elif config.architecture == "i386":
+        OVMF_VARS_LOCATIONS += [
+            "/usr/share/edk2/ovmf-ia32/OVMF_VARS.fd",
+            "/usr/share/OVMF/OVMF32_VARS_4M.fd",
+        ]
+    elif config.architecture == "armhfp":
+        OVMF_VARS_LOCATIONS += ["/usr/share/AAVMF/AAVMF32_VARS.fd"]
+    elif config.architecture == "aarch64":
+        OVMF_VARS_LOCATIONS += ["/usr/share/AAVMF/AAVMF_VARS.fd"]
+
+    OVMF_VARS_LOCATIONS += ["/usr/share/edk2/ovmf/OVMF_VARS.fd",
+                            "/usr/share/edk2-ovmf/OVMF_VARS.fd",  # GENTOO:
+                            "/usr/share/qemu/OVMF_VARS.fd",
+                            "/usr/share/ovmf/OVMF_VARS.fd",
+                            "/usr/share/OVMF/OVMF_VARS.fd"]
+
+    for location in OVMF_VARS_LOCATIONS:
+        if os.path.exists(location):
+            return Path(location)
+
+    die("Couldn't find OVMF UEFI variables file.")
+
+
+@contextlib.contextmanager
+def start_swtpm() -> Iterator[Optional[Path]]:
+
+    if not shutil.which("swtpm"):
+        yield None
+        return
+
+    with tempfile.TemporaryDirectory() as swtpm_state:
+        swtpm_sock = Path(swtpm_state) / Path("sock")
+
+        cmd = ["swtpm",
+               "socket",
+               "--tpm2",
+               "--tpmstate", f"dir={swtpm_state}",
+               "--ctrl", f"type=unixio,path={swtpm_sock}",
+         ]
+
+        swtpm_proc = spawn(cmd)
+
+        try:
+            yield swtpm_sock
+        finally:
+            swtpm_proc.wait()
+
+
+@contextlib.contextmanager
+def vsock_notify_handler() -> Iterator[tuple[str, dict[str, str]]]:
+    """
+    This yields a vsock address and a dict that will be filled in with the notifications from the VM. The
+    dict should only be accessed after the context manager has been finalized.
+    """
+    with socket.socket(socket.AF_VSOCK, socket.SOCK_SEQPACKET) as vsock:
+        vsock.bind((socket.VMADDR_CID_ANY, -1))
+        vsock.listen()
+        vsock.setblocking(False)
+
+        messages = {}
+
+        async def notify() -> None:
+            loop = asyncio.get_running_loop()
+
+            try:
+                while True:
+                    s, _ = await loop.sock_accept(vsock)
+
+                    for msg in (await loop.sock_recv(s, 4096)).decode().split("\n"):
+                        if not msg:
+                            continue
+
+                        k, _, v = msg.partition("=")
+                        messages[k] = v
+
+            except asyncio.CancelledError:
+                pass
+
+        with MkosiAsyncioThread(notify()):
+            yield f"vsock:{socket.VMADDR_CID_HOST}:{vsock.getsockname()[1]}", messages
+
+
+def run_qemu(args: MkosiArgs, config: MkosiConfig) -> None:
+    accel = "tcg"
+    if config.qemu_kvm == ConfigFeature.enabled or (config.qemu_kvm == ConfigFeature.auto and qemu_check_kvm_support()):
+        accel = "kvm"
+
+    firmware, fw_supports_sb = find_qemu_firmware(config)
+    smm = "on" if fw_supports_sb else "off"
+
+    if config.architecture == "aarch64":
+        machine = f"type=virt,accel={accel}"
+    else:
+        machine = f"type=q35,accel={accel},smm={smm}"
+
+    cmdline: list[PathString] = [
+        find_qemu_binary(config),
+        "-machine", machine,
+        "-smp", config.qemu_smp,
+        "-m", config.qemu_mem,
+        "-object", "rng-random,filename=/dev/urandom,id=rng0",
+        "-device", "virtio-rng-pci,rng=rng0,id=rng-device0",
+        "-nic", "user,model=virtio-net-pci",
+    ]
+
+    if qemu_check_vsock_support(log=True):
+        cmdline += ["-device", f"vhost-vsock-pci,guest-cid={machine_cid(config)}"]
+
+    cmdline += ["-cpu", "max"]
+
+    if config.qemu_gui:
+        cmdline += ["-vga", "virtio"]
+    else:
+        # -nodefaults removes the default CDROM device which avoids an error message during boot
+        # -serial mon:stdio adds back the serial device removed by -nodefaults.
+        cmdline += [
+            "-nographic",
+            "-nodefaults",
+            "-chardev", "stdio,mux=on,id=console,signal=off",
+            "-serial", "chardev:console",
+            "-mon", "console",
+        ]
+
+    for k, v in config.credentials.items():
+        cmdline += ["-smbios", f"type=11,value=io.systemd.credential.binary:{k}={base64.b64encode(v.encode()).decode()}"]
+    cmdline += ["-smbios", f"type=11,value=io.systemd.stub.kernel-cmdline-extra={' '.join(config.kernel_command_line_extra)}"]
+
+    cmdline += ["-drive", f"if=pflash,format=raw,readonly=on,file={firmware}"]
+
+    notifications: dict[str, str] = {}
+
+    with contextlib.ExitStack() as stack:
+        if fw_supports_sb:
+            ovmf_vars = stack.enter_context(tempfile.NamedTemporaryFile(prefix=".mkosi-", dir=tmp_dir()))
+            copy_path(find_ovmf_vars(config), Path(ovmf_vars.name))
+            cmdline += [
+                "-global", "ICH9-LPC.disable_s3=1",
+                "-global", "driver=cfi.pflash01,property=secure,value=on",
+                "-drive", f"file={ovmf_vars.name},if=pflash,format=raw",
+            ]
+
+        if config.ephemeral:
+            f = stack.enter_context(tempfile.NamedTemporaryFile(prefix=".mkosi-", dir=config.output_dir))
+            fname = Path(f.name)
+
+            # So on one hand we want CoW off, since this stuff will
+            # have a lot of random write accesses. On the other we
+            # want the copy to be snappy, hence we do want CoW. Let's
+            # ask for both, and let the kernel figure things out:
+            # let's turn off CoW on the file, but start with a CoW
+            # copy. On btrfs that works: the initial copy is made as
+            # CoW but later changes do not result in CoW anymore.
+
+            run(["chattr", "+C", fname], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
+            copy_path(config.output_dir / config.output, fname)
+        else:
+            fname = config.output_dir / config.output
+
+        # Debian images fail to boot with virtio-scsi, see: https://github.com/systemd/mkosi/issues/725
+        if config.output_format == OutputFormat.cpio:
+            kernel = config.output_dir / config.output_split_kernel
+            if not kernel.exists() and "-kernel" not in args.cmdline:
+                die("No kernel found, please install a kernel in the cpio or provide a -kernel argument to mkosi qemu")
+            cmdline += ["-kernel", kernel,
+                        "-initrd", fname,
+                        "-append", " ".join(config.kernel_command_line + config.kernel_command_line_extra)]
+        if config.distribution == Distribution.debian:
+            cmdline += ["-drive", f"if=virtio,id=hd,file={fname},format=raw"]
+        else:
+            cmdline += ["-drive", f"if=none,id=hd,file={fname},format=raw",
+                        "-device", "virtio-scsi-pci,id=scsi",
+                        "-device", "scsi-hd,drive=hd,bootindex=1"]
+
+        swtpm_socket = stack.enter_context(start_swtpm())
+        if swtpm_socket is not None:
+            cmdline += ["-chardev", f"socket,id=chrtpm,path={swtpm_socket}",
+                        "-tpmdev", "emulator,id=tpm0,chardev=chrtpm"]
+
+            if config.architecture == "x86_64":
+                cmdline += ["-device", "tpm-tis,tpmdev=tpm0"]
+            elif config.architecture == "aarch64":
+                cmdline += ["-device", "tpm-tis-device,tpmdev=tpm0"]
+
+        addr, notifications = stack.enter_context(vsock_notify_handler())
+        cmdline += ["-smbios", f"type=11,value=io.systemd.credential:vmm.notify_socket={addr}"]
+
+        cmdline += config.qemu_args
+        cmdline += args.cmdline
+
+        run(cmdline, stdin=sys.stdin, stdout=sys.stdout, env=os.environ, log=False)
+
+    if "EXIT_STATUS" in notifications:
+        raise subprocess.CalledProcessError(int(notifications["EXIT_STATUS"]), cmdline)
index 07feb02ada8ebb516ff6428b27a95a5f5e82632e..58e493ab5af8c08e5b7f4f9ea9badbc53f708f37 100644 (file)
@@ -3,8 +3,10 @@
 import ast
 import contextlib
 import enum
+import errno
 import functools
 import itertools
+import logging
 import os
 import pwd
 import re
@@ -281,3 +283,21 @@ def qemu_check_kvm_support() -> bool:
             return True
     except OSError:
         return False
+
+
+def qemu_check_vsock_support(log: bool) -> bool:
+    try:
+        os.open("/dev/vhost-vsock", os.O_RDWR|os.O_CLOEXEC)
+    except OSError as e:
+        if e.errno == errno.ENOENT:
+            if log:
+                logging.warning("/dev/vhost-vsock not found. Not adding a vsock device to the virtual machine.")
+            return False
+        elif e.errno in (errno.EPERM, errno.EACCES):
+            if log:
+                logging.warning("Permission denied to access /dev/vhost-vsock. Not adding a vsock device to the virtual machine.")
+            return False
+
+        raise e
+
+    return True