import resource
import shlex
import shutil
+import socket
import stat
import subprocess
import sys
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,
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
ssh_key: Optional[Path]
ssh_certificate: Optional[Path]
machine: Optional[str]
+ forward_journal: Optional[Path]
vmm: Vmm
# QEMU-specific options
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",
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)}
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):
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):
squashfs-tools
swtpm-tools
systemd-container
+ systemd-journal-remote
systemd-udev
ubu-keyring
virt-firmware
systemd-boot
systemd-container
systemd-coredump
+ systemd-journal-remote
ubuntu-keyring
uidmap
xz-utils
systemd-container
systemd-coredump
systemd-experimental
+ systemd-journal-remote
xz
zypper
: 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
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)
"ExtraTrees": [],
"FinalizeScripts": [],
"Format": "uki",
+ "ForwardJournal": "/mkosi.journal",
"Hostname": null,
"Image": "default",
"ImageId": "myimage",
extra_search_paths = [],
extra_trees = [],
finalize_scripts = [],
+ forward_journal = Path("/mkosi.journal"),
hostname = None,
vmm = Vmm.qemu,
image = "default",