From 045d64a4a3cc773de6c6faed4a46b636542b2845 Mon Sep 17 00:00:00 2001 From: Nathan Chancellor Date: Fri, 17 Feb 2023 19:41:10 -0700 Subject: [PATCH] boot-qemu.py: Add support for mounting a folder into the guest via virtiofs virtiofs, available in QEMU 5.2 or newer and Linux guests 5.4 or newer, is a more modern way to pass local folders along to QEMU, as it takes advantage of the fact that the folders are on the same machine as the hypervisor. To use virtiofs, we first need to run virtiofsd, which is included with most base QEMU packages. Once we find it, we run it in the background and connect to it using some QEMU parameters, which were shamelessly taken from the official virtiofs website: https://virtio-fs.gitlab.io/howto-qemu.html To use it within the guest (you can use a different path than /mnt/shared but 'mount -t virtio shared' must be used): # mkdir /mnt/shared # mount -t virtiofs shared /mnt/shared # echo "$(uname -a)" >/mnt/shared/foo On the host: $ cat shared/foo Linux (none) 6.1.0-rc8-next-20221207 #2 SMP PREEMPT Wed Dec 7 14:56:03 MST 2022 aarch64 GNU/Linux This does require guest kernel support (CONFIG_VIRTIO_FS=y), otherwise it will not work inside the guest: / # mount -t virtiofs shared /mnt/shared mount: mounting shared on /mnt/shared failed: No such device Link: https://github.com/ClangBuiltLinux/boot-utils/issues/81 Signed-off-by: Nathan Chancellor --- .gitignore | 2 + boot-qemu.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 105 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index b8bf959..ef91c63 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ qemu-binaries/ *.pyc +shared/ +.vfsd.* diff --git a/boot-qemu.py b/boot-qemu.py index e17e9ad..dd26659 100755 --- a/boot-qemu.py +++ b/boot-qemu.py @@ -3,6 +3,7 @@ from argparse import ArgumentParser import contextlib +import grp import os from pathlib import Path import platform @@ -15,6 +16,7 @@ import utils BOOT_UTILS = Path(__file__).resolve().parent +SHARED_FOLDER = Path(BOOT_UTILS, 'shared') SUPPORTED_ARCHES = [ 'arm', 'arm32_v5', @@ -49,6 +51,7 @@ def __init__(self): self.kernel = None self.kernel_config = None self.kernel_dir = None + self.share_folder_with_guest = False self.smp = 0 self.timeout = '' # It may be tempting to use self.use_kvm during initialization of @@ -73,6 +76,14 @@ def __init__(self): ] # yapf: disable self._qemu_path = None self._ram = '512m' + self._vfsd_conf = { + 'cmd': [], + 'files': { + 'log': Path(BOOT_UTILS, '.vfsd.log'), + 'mem': Path(BOOT_UTILS, '.vfsd.mem'), + 'sock': Path(BOOT_UTILS, '.vfsd.sock'), + }, + } def _can_use_kvm(self): return False @@ -187,6 +198,52 @@ def _prepare_initrd(self): return dst + def _prepare_for_shared_folder(self): + if self._get_kernel_config_val('CONFIG_VIRTIO_FS') != 'y': + utils.yellow( + 'CONFIG_VIRTIO_FS may not be enabled in your configuration, shared folder may not work...', + ) + + # Print information about using shared folder + utils.green('To mount shared folder in guest (e.g. to /mnt/shared):') + utils.green('\t/ # mkdir /mnt/shared') + utils.green('\t/ # mount -t virtiofs shared /mnt/shared') + + SHARED_FOLDER.mkdir(exist_ok=True, parents=True) + + # Make sure sudo is available and we have permission to use it + if not (sudo := shutil.which('sudo')): + raise FileNotFoundError( + 'sudo is required to use virtiofsd but it could not be found!') + utils.green( + 'Requesting sudo permission to run virtiofsd in the background...') + subprocess.run([sudo, 'true'], check=True) + + # Find virtiofsd relative to QEMU's prefix + qemu_prefix = self._qemu_path.resolve().parents[1] + virtiofsd_locations = [ + Path('libexec', 'virtiofsd'), # Default QEMU installation, Fedora + Path('lib', 'qemu', 'virtiofsd'), # Arch Linux, Debian, Ubuntu + ] + virtiofsd = utils.find_first_file(qemu_prefix, virtiofsd_locations) + + # Prepare QEMU arguments + self._qemu_args += [ + '-chardev', f"socket,id=char0,path={self._vfsd_conf['files']['sock']}", + '-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=shared', + '-object', f"memory-backend-file,id=shm,mem-path={self._vfsd_conf['files']['mem']},share=on,size={self._ram}", + '-numa', 'node,memdev=shm', + ] # yapf: disable + + self._vfsd_conf['cmd'] = [ + sudo, + virtiofsd, + f"--socket-group={grp.getgrgid(os.getgid()).gr_name}", + f"--socket-path={self._vfsd_conf['files']['sock']}", + '-o', f"source={SHARED_FOLDER}", + '-o', 'cache=always', + ] # yapf: disable + def _run_gdb(self): qemu_cmd = [self._qemu_path, *self._qemu_args] @@ -223,6 +280,9 @@ def _run_gdb(self): break def _run_fg(self): + if self.share_folder_with_guest: + self._prepare_for_shared_folder() + # Pretty print and run QEMU command qemu_cmd = [] @@ -236,14 +296,31 @@ def _run_fg(self): qemu_cmd += [self._qemu_path, *self._qemu_args] print(f"\n$ {' '.join(shlex.quote(str(elem)) for elem in qemu_cmd)}") - try: - subprocess.run(qemu_cmd, check=True) - except subprocess.CalledProcessError as err: - if err.returncode == 124: - utils.red("ERROR: QEMU timed out!") - else: - utils.red("ERROR: QEMU did not exit cleanly!") - sys.exit(err.returncode) + null_cm = contextlib.nullcontext() + with self._vfsd_conf['files']['log'].open('w', encoding='utf-8') if self.share_folder_with_guest else null_cm as vfsd_log, \ + subprocess.Popen(self._vfsd_conf['cmd'], stderr=vfsd_log, stdout=vfsd_log) if self.share_folder_with_guest else null_cm as vfsd_proc: + try: + subprocess.run(qemu_cmd, check=True) + except subprocess.CalledProcessError as err: + if err.returncode == 124: + utils.red("ERROR: QEMU timed out!") + else: + utils.red("ERROR: QEMU did not exit cleanly!") + # If virtiofsd is dead, it is pretty likely that it was the + # cause of QEMU failing so add to the existing exception using + # 'from'. + if vfsd_proc and vfsd_proc.poll(): + # yapf: disable + vfsd_log_txt = self._vfsd_conf['files']['log'].read_text(encoding='utf-8') + raise RuntimeError(f"virtiofsd failed with: {vfsd_log_txt}") from err + # yapf: enable + sys.exit(err.returncode) + finally: + if vfsd_proc: + vfsd_proc.kill() + # Delete the memory to save space, it does not have to be + # persistent + self._vfsd_conf['files']['mem'].unlink(missing_ok=True) def _set_kernel_vars(self): if self.kernel: @@ -719,6 +796,12 @@ def parse_arguments(): help= 'Number of processors for virtual machine (default: only KVM machines will use multiple vCPUs.)', ) + parser.add_argument( + '--share-folder-with-guest', + action='store_true', + help= + f"Share {SHARED_FOLDER} with the guest using virtiofs (requires interactive, not supported with gdb).", + ) parser.add_argument('-t', '--timeout', default='3m', @@ -780,6 +863,18 @@ def parse_arguments(): if args.no_kvm: runner.use_kvm = False + if args.share_folder_with_guest: + if not args.interactive: + utils.yellow( + 'Shared folder requested without an interactive session, ignoring...', + ) + elif args.gdb: + utils.yellow( + 'Shared folder requested during a debugging session, ignoring...', + ) + else: + runner.share_folder_with_guest = True + if args.smp: runner.smp = args.smp