Skip to content

Commit 9b69872

Browse files
committed
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 find and run virtiofsd, which has two different implementations: a C implementation included with QEMU up until 8.0 (available on most distros) and a standalone Rust implementation available on GitLab (not packaged on many distros but easy to build and install). 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 Closes: #81 Link: https://gitlab.com/virtio-fs/virtiofsd Signed-off-by: Nathan Chancellor <[email protected]>
1 parent 0edf02d commit 9b69872

File tree

2 files changed

+122
-12
lines changed

2 files changed

+122
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
images/
22
qemu-binaries/
33
*.pyc
4+
shared/
5+
.vfsd.*

boot-qemu.py

Lines changed: 120 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from argparse import ArgumentParser
55
import contextlib
6+
import grp
67
import os
78
from pathlib import Path
89
import platform
@@ -14,6 +15,7 @@
1415

1516
import utils
1617

18+
SHARED_FOLDER = Path(utils.BOOT_UTILS, 'shared')
1719
SUPPORTED_ARCHES = [
1820
'arm',
1921
'arm32_v5',
@@ -40,7 +42,7 @@ class QEMURunner:
4042

4143
def __init__(self):
4244

43-
# Properties that can be adjusted by the user or class
45+
# Properties that can be adjusted by the user or class (keep alphabetized if possible)
4446
self.cmdline = []
4547
self.efi = False
4648
self.gdb = False
@@ -51,14 +53,15 @@ def __init__(self):
5153
self.kernel_config = None
5254
self.kernel_dir = None
5355
self.memory = '512m'
56+
self.share_folder_with_guest = False
57+
self.smp = 0
5458
self.supports_efi = False
59+
self.timeout = ''
5560
# It may be tempting to use self.use_kvm during initialization of
5661
# subclasses to set certain properties but the user can explicitly opt
5762
# out of KVM after instantiation, so any decisions based on it should
5863
# be confined to run().
5964
self.use_kvm = False
60-
self.smp = 0
61-
self.timeout = ''
6265

6366
self._default_kernel_path = None
6467
self._dtbs = []
@@ -72,6 +75,14 @@ def __init__(self):
7275
'-nodefaults',
7376
] # yapf: disable
7477
self._qemu_path = None
78+
self._vfsd_conf = {
79+
'cmd': [],
80+
'files': {
81+
'log': Path(utils.BOOT_UTILS, '.vfsd.log'),
82+
'mem': Path(utils.BOOT_UTILS, '.vfsd.mem'),
83+
'sock': Path(utils.BOOT_UTILS, '.vfsd.sock'),
84+
},
85+
}
7586

7687
def _find_dtb(self):
7788
if not self._dtbs:
@@ -173,13 +184,75 @@ def _get_qemu_ver_tuple(self):
173184
def _have_dev_kvm_access(self):
174185
return os.access('/dev/kvm', os.R_OK | os.W_OK)
175186

187+
def _prepare_for_shared_folder(self):
188+
if self._get_kernel_config_val('CONFIG_VIRTIO_FS') != 'y':
189+
utils.yellow(
190+
'CONFIG_VIRTIO_FS may not be enabled in your configuration, shared folder may not work...'
191+
)
192+
193+
# Print information about using shared folder
194+
utils.green('To mount shared folder in guest (e.g. to /mnt/shared):')
195+
utils.green('\t/ # mkdir /mnt/shared')
196+
utils.green('\t/ # mount -t virtiofs shared /mnt/shared')
197+
198+
SHARED_FOLDER.mkdir(exist_ok=True, parents=True)
199+
200+
# Make sure sudo is available and we have permission to use it
201+
if not (sudo := shutil.which('sudo')):
202+
raise FileNotFoundError(
203+
'sudo is required to use virtiofsd but it could not be found!')
204+
utils.green(
205+
'Requesting sudo permission to run virtiofsd in the background...')
206+
subprocess.run([sudo, 'true'], check=True)
207+
208+
# There are two implementations of virtiofsd. The original C
209+
# implementation was bundled and built with QEMU up until 8.0, where it
210+
# was removed after being deprecated in 7.0:
211+
#
212+
# https://lore.kernel.org/[email protected]/
213+
#
214+
# The standalone Rust implementation is preferred now, which should be
215+
# available in PATH. If it is not available, see if there is a C
216+
# implementation available in QEMU's prefix.
217+
if not (virtiofsd := shutil.which('virtiofsd')):
218+
utils.yellow(
219+
'Could not find Rust implementation of virtiofsd (https://gitlab.com/virtio-fs/virtiofsd), searching for old C implementation...'
220+
)
221+
222+
qemu_prefix = self._qemu_path.resolve().parents[1]
223+
virtiofsd_locations = [
224+
Path('libexec/virtiofsd'), # Default QEMU installation, Fedora
225+
Path('lib/qemu/virtiofsd'), # Arch Linux, Debian, Ubuntu
226+
]
227+
virtiofsd = utils.find_first_file(qemu_prefix, virtiofsd_locations)
228+
229+
# Prepare QEMU arguments
230+
self._qemu_args += [
231+
'-chardev', f"socket,id=char0,path={self._vfsd_conf['files']['sock']}",
232+
'-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char0,tag=shared',
233+
'-object', f"memory-backend-file,id=shm,mem-path={self._vfsd_conf['files']['mem']},share=on,size={self._ram}",
234+
'-numa', 'node,memdev=shm',
235+
] # yapf: disable
236+
237+
self._vfsd_conf['cmd'] = [
238+
sudo,
239+
virtiofsd,
240+
f"--socket-group={grp.getgrgid(os.getgid()).gr_name}",
241+
f"--socket-path={self._vfsd_conf['files']['sock']}",
242+
'-o', f"source={SHARED_FOLDER}",
243+
'-o', 'cache=always',
244+
] # yapf: disable
245+
176246
def _prepare_initrd(self):
177247
if not self._initrd_arch:
178248
raise RuntimeError('No initrd architecture specified?')
179249
return utils.prepare_initrd(self._initrd_arch,
180250
gh_json_file=self.gh_json_file)
181251

182252
def _run_fg(self):
253+
if self.share_folder_with_guest:
254+
self._prepare_for_shared_folder()
255+
183256
# Pretty print and run QEMU command
184257
qemu_cmd = []
185258

@@ -192,15 +265,32 @@ def _run_fg(self):
192265

193266
qemu_cmd += [self._qemu_path, *self._qemu_args]
194267

195-
print(f"$ {' '.join(shlex.quote(str(elem)) for elem in qemu_cmd)}")
196-
try:
197-
subprocess.run(qemu_cmd, check=True)
198-
except subprocess.CalledProcessError as err:
199-
if err.returncode == 124:
200-
utils.red("ERROR: QEMU timed out!")
201-
else:
202-
utils.red("ERROR: QEMU did not exit cleanly!")
203-
sys.exit(err.returncode)
268+
print(f"\n$ {' '.join(shlex.quote(str(elem)) for elem in qemu_cmd)}")
269+
null_cm = contextlib.nullcontext()
270+
with self._vfsd_conf['files']['log'].open('w', encoding='utf-8') if self.share_folder_with_guest else null_cm as vfsd_log, \
271+
subprocess.Popen(self._vfsd_conf['cmd'], stderr=vfsd_log, stdout=vfsd_log) if self.share_folder_with_guest else null_cm as vfsd_proc:
272+
try:
273+
subprocess.run(qemu_cmd, check=True)
274+
except subprocess.CalledProcessError as err:
275+
if err.returncode == 124:
276+
utils.red("ERROR: QEMU timed out!")
277+
else:
278+
utils.red("ERROR: QEMU did not exit cleanly!")
279+
# If virtiofsd is dead, it is pretty likely that it was the
280+
# cause of QEMU failing so add to the existing exception using
281+
# 'from'.
282+
if vfsd_proc and vfsd_proc.poll():
283+
# yapf: disable
284+
vfsd_log_txt = self._vfsd_conf['files']['log'].read_text(encoding='utf-8')
285+
raise RuntimeError(f"virtiofsd failed with: {vfsd_log_txt}") from err
286+
# yapf: enable
287+
sys.exit(err.returncode)
288+
finally:
289+
if vfsd_proc:
290+
vfsd_proc.kill()
291+
# Delete the memory to save space, it does not have to be
292+
# persistent
293+
self._vfsd_conf['files']['mem'].unlink(missing_ok=True)
204294

205295
def _run_gdb(self):
206296
qemu_cmd = [self._qemu_path, *self._qemu_args]
@@ -846,6 +936,12 @@ def parse_arguments():
846936
help=
847937
'Number of processors for virtual machine (default: only KVM machines will use multiple vCPUs.)',
848938
)
939+
parser.add_argument(
940+
'--share-folder-with-guest',
941+
action='store_true',
942+
help=
943+
f"Share {SHARED_FOLDER} with the guest using virtiofs (requires interactive, not supported with gdb).",
944+
)
849945
parser.add_argument('-t',
850946
'--timeout',
851947
default='3m',
@@ -918,6 +1014,18 @@ def parse_arguments():
9181014
if args.no_kvm:
9191015
runner.use_kvm = False
9201016

1017+
if args.share_folder_with_guest:
1018+
if args.gdb:
1019+
utils.yellow(
1020+
'Shared folder requested during a debugging session, ignoring...'
1021+
)
1022+
elif not args.interactive:
1023+
utils.yellow(
1024+
'Shared folder requested without an interactive session, ignoring...'
1025+
)
1026+
else:
1027+
runner.share_folder_with_guest = True
1028+
9211029
if args.smp:
9221030
runner.smp = args.smp
9231031

0 commit comments

Comments
 (0)