]> git.ipfire.org Git - thirdparty/mkosi.git/commitdiff
Add ForwardJournal= to enable log forwarding of VMs and containers 2572/head
authorDaan De Meyer <daan.j.demeyer@gmail.com>
Sun, 31 Mar 2024 17:54:22 +0000 (19:54 +0200)
committerDaan De Meyer <daan.j.demeyer@gmail.com>
Tue, 2 Apr 2024 10:18:14 +0000 (12:18 +0200)
In systemd v256, journald will support forwarding to systemd-journal-remote
via the new journal.forward_to_socket credential. Let's expose this
functionality via a new ForwardJournal= setting, which specifies a path to
which logs should be forwarded.

mkosi/__init__.py
mkosi/config.py
mkosi/qemu.py
mkosi/resources/mkosi-tools/mkosi.conf.d/10-centos-fedora/mkosi.conf
mkosi/resources/mkosi-tools/mkosi.conf.d/10-debian-ubuntu.conf
mkosi/resources/mkosi-tools/mkosi.conf.d/10-opensuse.conf
mkosi/resources/mkosi.md
mkosi/vmspawn.py
tests/test_json.py

index 67493ad016d34460e3099a221188dcabc9b3f492..135f1528675441d5ed7fe4a2e31490900a10d5cc 100644 (file)
@@ -12,6 +12,7 @@ import os
 import resource
 import shlex
 import shutil
+import socket
 import stat
 import subprocess
 import sys
@@ -59,7 +60,7 @@ from mkosi.manifest import Manifest
 from mkosi.mounts import finalize_source_mounts, mount_overlay
 from mkosi.pager import page
 from mkosi.partition import Partition, finalize_root, finalize_roothash
-from mkosi.qemu import KernelType, copy_ephemeral, run_qemu, run_ssh
+from mkosi.qemu import KernelType, copy_ephemeral, run_qemu, run_ssh, start_journal_remote
 from mkosi.run import (
     find_binary,
     fork_and_wait,
@@ -3778,6 +3779,19 @@ def run_shell(args: Args, config: Config) -> None:
             os.chmod(scratch, 0o1777)
             cmdline += ["--bind", f"{scratch}:/var/tmp"]
 
+        if args.verb == Verb.boot and config.forward_journal:
+            with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock:
+                addr = Path(os.getenv("TMPDIR", "/tmp")) / f"mkosi-journal-remote-unix-{uuid.uuid4().hex[:16]}"
+                sock.bind(os.fspath(addr))
+                sock.listen()
+                if config.output_format == OutputFormat.directory and (stat := os.stat(fname)).st_uid != 0:
+                    os.chown(addr, stat.st_uid, stat.st_gid)
+                stack.enter_context(start_journal_remote(config, sock.fileno()))
+                cmdline += [
+                    "--bind", f"{addr}:/run/host/journal/socket",
+                    "--set-credential=journal.forward_to_socket:/run/host/journal/socket",
+                ]
+
         if args.verb == Verb.boot:
             # Add nspawn options first since systemd-nspawn ignores all options after the first argument.
             cmdline += args.cmdline
index aa8696957e29de020ce4cf45f146ccde85c516ef..4fcd6e9f894cae1eff8ccd1ff5963e7b633d77d9 100644 (file)
@@ -1434,6 +1434,7 @@ class Config:
     ssh_key: Optional[Path]
     ssh_certificate: Optional[Path]
     machine: Optional[str]
+    forward_journal: Optional[Path]
     vmm: Vmm
 
     # QEMU-specific options
@@ -2780,6 +2781,13 @@ SETTINGS = (
         section="Host",
         help="Set the machine name to use when booting the image",
     ),
+    ConfigSetting(
+        dest="forward_journal",
+        metavar="PATH",
+        section="Host",
+        parse=config_make_path_parser(required=False),
+        help="Set the path used to store forwarded machine journals",
+    ),
     ConfigSetting(
         dest="qemu_gui",
         metavar="BOOL",
@@ -3892,6 +3900,7 @@ def summary(config: Config) -> str:
                     SSH Signing Key: {none_to_none(config.ssh_key)}
                     SSH Certificate: {none_to_none(config.ssh_certificate)}
                             Machine: {config.machine_or_name()}
+                    Forward Journal: {none_to_none(config.forward_journal)}
 
             Virtual Machine Monitor: {config.vmm}
                            QEMU GUI: {yes_no(config.qemu_gui)}
index 64820f7e4aaaab0076b52eb46ab203573e3d0380..3ca175d195b2029db64f5b45d9a6ed67de8ab11e 100644 (file)
@@ -411,6 +411,48 @@ def vsock_notify_handler() -> Iterator[tuple[str, dict[str, str]]]:
             logging.debug(f"- {k}={v}")
 
 
+@contextlib.contextmanager
+def start_journal_remote(config: Config, sockfd: int) -> Iterator[None]:
+    assert config.forward_journal
+
+    bin = find_binary("systemd-journal-remote", "/usr/lib/systemd/systemd-journal-remote", root=config.tools())
+    if not bin:
+        die("systemd-journal-remote must be installed to forward logs from the virtual machine")
+
+    d = config.forward_journal.parent if config.forward_journal.suffix == ".journal" else config.forward_journal
+    if not d.exists():
+        d.mkdir(parents=True)
+        # Make sure COW is disabled so systemd-journal-remote doesn't complain on btrfs filesystems.
+        run(["chattr", "+C", d], check=False, stderr=subprocess.DEVNULL if not ARG_DEBUG.get() else None)
+        INVOKING_USER.chown(d)
+
+    with spawn(
+        [
+            bin,
+            "--output", config.forward_journal,
+            "--split-mode", "none" if config.forward_journal.suffix == ".journal" else "host",
+        ],
+        pass_fds=(sockfd,),
+        sandbox=config.sandbox(mounts=[Mount(config.forward_journal.parent, config.forward_journal.parent)]),
+        user=config.forward_journal.parent.stat().st_uid,
+        group=config.forward_journal.parent.stat().st_gid,
+        # If all logs go into a single file, disable compact mode to allow for journal files exceeding 4G.
+        env={"SYSTEMD_JOURNAL_COMPACT": "0" if config.forward_journal.suffix == ".journal" else "1"},
+    ) as proc:
+        yield
+        proc.terminate()
+
+
+@contextlib.contextmanager
+def start_journal_remote_vsock(config: Config) -> Iterator[str]:
+    with socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM) as sock:
+        sock.bind((socket.VMADDR_CID_ANY, socket.VMADDR_PORT_ANY))
+        sock.listen()
+
+        with start_journal_remote(config, sock.fileno()):
+            yield f"vsock-stream:{socket.VMADDR_CID_HOST}:{sock.getsockname()[1]}"
+
+
 @contextlib.contextmanager
 def copy_ephemeral(config: Config, src: Path) -> Iterator[Path]:
     if not config.ephemeral or config.output_format in (OutputFormat.cpio, OutputFormat.uki):
@@ -957,6 +999,9 @@ def run_qemu(args: Args, config: Config) -> None:
             addr, notifications = stack.enter_context(vsock_notify_handler())
             credentials["vmm.notify_socket"] = addr
 
+        if config.forward_journal:
+            credentials["journal.forward_to_socket"] = stack.enter_context(start_journal_remote_vsock(config))
+
         for k, v in credentials.items():
             payload = base64.b64encode(v.encode()).decode()
             if config.architecture.supports_smbios(firmware):
index 53974278863dd734a9e4e11aceb23411d93ea0d0..dd5a2ccc04ec2092584d2e0074735cd303000c8a 100644 (file)
@@ -24,6 +24,7 @@ Packages=
         squashfs-tools
         swtpm-tools
         systemd-container
+        systemd-journal-remote
         systemd-udev
         ubu-keyring
         virt-firmware
index ce27a589a975f15b468405721e6e49d132e90baf..457a426f9ce46fe5c9ed0d1921d1dec34f258acb 100644 (file)
@@ -31,6 +31,7 @@ Packages=
         systemd-boot
         systemd-container
         systemd-coredump
+        systemd-journal-remote
         ubuntu-keyring
         uidmap
         xz-utils
index 28bed89f6e38677e4f5460e9928103b5bac73546..8034481b31761adb6af25dc07b695cf808e2cfe3 100644 (file)
@@ -27,5 +27,6 @@ Packages=
         systemd-container
         systemd-coredump
         systemd-experimental
+        systemd-journal-remote
         xz
         zypper
index f3e1d55ed984fcbc6dca54cdab4f5407a9800ebd..17299b6ecf3dc44dc0a3233070045ba1b56f4e26 100644 (file)
@@ -1845,6 +1845,17 @@ boolean argument: either `1`, `yes`, or `true` to enable, or `0`, `no`,
 : Note that `Ephemeral=` has to be enabled to start multiple instances
   of the same image.
 
+`ForwardJournal=`, `--forward-journal=`
+
+: Specify the path to which journal logs from containers and virtual
+  machines should be forwarded. If the path has the `.journal`
+  extension, it is interpreted as a file to which the journal should be
+  written. Otherwise, the path is interpreted as a directory to which
+  the journal should be written.
+
+: Note that systemd v256 or newer is required in the virtual machine for
+  log forwarding to work.
+
 ## Specifiers
 
 The current value of various settings can be accessed when parsing
index f8daa5aa2d5da331332aec9abe387f2724d00b30..ee1701ca04c59eb3fddf023a04ba148169d47812 100644 (file)
@@ -93,6 +93,9 @@ def run_vmspawn(args: Args, config: Config) -> None:
         else:
             cmdline += ["--image", fname]
 
+        if config.forward_journal:
+            cmdline += ["--forward-journal", config.forward_journal]
+
         cmdline += [*args.cmdline, *config.kernel_command_line_extra]
 
         run(cmdline, stdin=sys.stdin, stdout=sys.stdout, env=os.environ | config.environment, log=False)
index 768a37eb1d75d95c136175e41b635bc4c35e4989..e36c41aed512584b4e348a32d68b8f44401c97b0 100644 (file)
@@ -138,6 +138,7 @@ def test_config() -> None:
             "ExtraTrees": [],
             "FinalizeScripts": [],
             "Format": "uki",
+            "ForwardJournal": "/mkosi.journal",
             "Hostname": null,
             "Image": "default",
             "ImageId": "myimage",
@@ -367,6 +368,7 @@ def test_config() -> None:
         extra_search_paths = [],
         extra_trees = [],
         finalize_scripts = [],
+        forward_journal = Path("/mkosi.journal"),
         hostname = None,
         vmm = Vmm.qemu,
         image = "default",