blob: 78782caa53c4d97bc152eea6443029a6b86e9540 [file] [log] [blame]
# -*- mode: python -*-
# virtme-run: The main command-line virtme frontend
# Copyright © 2014 Andy Lutomirski
# Licensed under the GPLv2, which is available in the virtme distribution
# as a file called LICENSE with SHA-256 hash:
# 8177f97513213526df2cf6184d8ff986c675afb514d4e68a404010521b880643
from typing import Any, Optional, List, NoReturn, Dict
import argparse
import tempfile
import os
import errno
import fcntl
import sys
import shlex
import re
import itertools
import subprocess
from .. import virtmods
from .. import modfinder
from .. import mkinitramfs
from .. import qemu_helpers
from .. import architectures
from .. import resources
uname = os.uname()
class SilentError(Exception):
def make_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description='Virtualize your system (or another) under a kernel image',
g: Any
g = parser.add_argument_group(title='Selection of kernel and modules').add_mutually_exclusive_group()
g.add_argument('--installed-kernel', action='store', nargs='?',
const=uname.release, default=None, metavar='VERSION',
help='Use an installed kernel and its associated modules. If no version is specified, the running kernel will be used.')
g.add_argument('--kimg', action='store',
help='Use specified kernel image with no modules.')
g.add_argument('--kdir', action='store', metavar='KDIR',
help='Use a compiled kernel source directory')
g = parser.add_argument_group(title='Kernel options')
g.add_argument('--mods', action='store', choices=['none', 'use', 'auto'], default='use',
help='Setup loadable kernel modules inside a compiled kernel source directory (used in conjunction with --kdir); none: ignore kernel modules, use: asks user to refresh virtme\'s kernel modules directory, auto: automatically refreshes virtme\'s kernel modules directory')
g.add_argument('-a', '--kopt', action='append', default=[],
help='Add a kernel option. You can specify this more than once.')
g.add_argument('--xen', action='store',
help='Boot Xen using the specified uncompressed hypervisor.')
g = parser.add_argument_group(title='Common guest options')
g.add_argument('--root', action='store', default='/',
help='Local path to use as guest root')
g.add_argument('--rw', action='store_true',
help='Give the guest read-write access to its root filesystem')
g.add_argument('--graphics', action='store_true',
help='Show graphical output instead of using a console.')
g.add_argument('--net', action='store', const='user', nargs='?',
choices=['user', 'bridge'],
help='Enable basic network access.')
g.add_argument('--balloon', action='store_true',
help='Allow the host to ask the guest to release memory.')
g.add_argument('--disk', action='append', default=[], metavar='NAME=PATH',
help='Add a read/write virtio-scsi disk. The device node will be /dev/disk/by-id/scsi-0virtme_disk_NAME.')
g.add_argument('--memory', action='store', default=None,
help='Set guest memory and qemu -m flag.')
g.add_argument('--name', action='store', default=None,
help='Set guest hostname and qemu -name flag.')
g = parser.add_argument_group(
description="Using any of the scripting options will run a script in the guest. The script's stdin will be attached to virtme-run's stdin and the script's stdout and stderr will both be attached to virtme-run's stdout. Kernel logs will go to stderr. This behaves oddly if stdin is a terminal; try using 'cat |virtme-run' if you have trouble with script mode.")
g.add_argument('--script-sh', action='store', metavar='SHELL_COMMAND',
help='Run a one-line shell script in the guest.')
g.add_argument('--script-exec', action='store', metavar='BINARY',
help='Run the specified binary in the guest.')
g = parser.add_argument_group(
description="Options related to architecture selection")
g.add_argument('--arch', action='store', metavar='ARCHITECTURE',
help='Guest architecture')
g.add_argument('--busybox', action='store', metavar='PATH_TO_BUSYBOX',
help='Use the specified busybox binary.')
g = parser.add_argument_group(title='Virtualizer settings')
g.add_argument('-q', '--qemu-opt', action='append', default=[],
help="Add a single QEMU argument. Use this when --qemu-opts's greedy behavior is problematic.'")
g.add_argument('--qemu-opts', action='store', nargs=argparse.REMAINDER,
metavar='OPTS...', help='Additional arguments for QEMU. This will consume all remaining arguments, so it must be specified last. Avoid using -append; use --kopt instead.')
g = parser.add_argument_group(title='Debugging/testing')
g.add_argument('--force-initramfs', action='store_true',
help='Use an initramfs even if unnecessary')
g.add_argument('--dry-run', action='store_true',
help="Initialize everything but don't run the guest")
g.add_argument('--show-command', action='store_true',
help='Show the VM command line')
g.add_argument('--show-boot-console', action='store_true',
help='Show the boot console when running scripts')
g = parser.add_argument_group(title='Guest userspace configuration')
g.add_argument('--pwd', action='store_true',
help='Propagate current working directory to the guest')
g.add_argument('--cwd', action='store',
help='Change guest working directory')
g = parser.add_argument_group(title='Sharing resources with guest')
g.add_argument('--rwdir', action='append', default=[],
help="Supply a read/write directory to the guest. Use --rwdir=path or --rwdir=guestpath=hostpath.")
g.add_argument('--rodir', action='append', default=[],
help="Supply a read-only directory to the guest. Use --rodir=path or --rodir=guestpath=hostpath.")
return parser
_ARGPARSER = make_parser()
def arg_fail(message, show_usage=True) -> NoReturn:
if show_usage:
def is_file_more_recent(a, b) -> bool:
return os.stat(a).st_mtime > os.stat(b).st_mtime
class Kernel:
__slots__ = ['kimg', 'dtb', 'modfiles', 'moddir', 'use_root_mods', 'config']
kimg: str
dtb: Optional[str]
modfiles: List[str]
moddir: Optional[str]
use_root_mods: bool
config: Optional[Dict[str, str]]
def load_config(self, kdir: str) -> None:
cfgfile = os.path.join(kdir, '.config')
if os.path.isfile(cfgfile):
self.config = {}
regex = re.compile('^(CONFIG_[A-Z0-9_]+)=([ymn])$')
for line in open(cfgfile, 'r'):
m = regex.match(line.strip())
if m:
self.config[] =
def find_kernel_and_mods(arch, args) -> Kernel:
kernel = Kernel()
kernel.use_root_mods = False
if args.installed_kernel is not None:
kver = args.installed_kernel
if args.mods != 'none':
kernel.modfiles = modfinder.find_modules_from_install(
virtmods.MODALIASES, kver=kver)
kernel.moddir = os.path.join('/lib/modules', kver)
kernel.modfiles = []
kernel.moddir = None
kernel.kimg = '/usr/lib/modules/%s/vmlinuz' % kver
if not os.path.exists(kernel.kimg):
kernel.kimg = '/boot/vmlinuz-%s' % kver
kernel.dtb = None # For now
kernel.use_root_mods = True
elif args.kdir is not None:
kernel.kimg = os.path.join(args.kdir, arch.kimg_path())
virtme_mods = os.path.join(args.kdir, '.virtme_mods')
mod_file = os.path.join(args.kdir, 'modules.order')
virtme_mod_file = os.path.join(virtme_mods, 'lib/modules/0.0.0/modules.dep')
# Kernel modules support
kver = None
kernel.moddir = None
kernel.modfiles = []
modmode = args.mods
if kernel.config is not None and kernel.config.get('CONFIG_MODULES', 'n') != 'y':
modmode = 'none'
if modmode == 'none':
elif modmode == 'use' or modmode == 'auto':
# Check if modules.order exists, otherwise it's not possible to use
# this option
if not os.path.exists(mod_file):
arg_fail('%s not found: kernel modules not enabled or kernel not compiled properly' % mod_file, show_usage=False)
# Check if virtme's kernel modules directory needs to be updated
if not os.path.exists(virtme_mods) or \
is_file_more_recent(mod_file, virtme_mod_file):
if modmode == 'use':
# Inform user to manually refresh virtme's kernel modules
# directory
arg_fail("please run virtme-prep-kdir-mods to update virtme's kernel modules directory or use --mods=auto", show_usage=False)
# Auto-refresh virtme's kernel modules directory
except subprocess.CalledProcessError:
raise SilentError()
kernel.moddir = os.path.join(virtme_mods, 'lib/modules', '0.0.0')
kernel.modfiles = modfinder.find_modules_from_install(
virtmods.MODALIASES, root=virtme_mods, kver='0.0.0')
arg_fail("invalid argument '%s', please use --mods=none|use|auto" % args.mods)
dtb_path = arch.dtb_path()
if dtb_path is None:
kernel.dtb = None
kernel.dtb = os.path.join(args.kdir, dtb_path)
elif args.mods is not None:
arg_fail("--mods must be used together with --kdir")
elif args.kimg is not None:
kernel.kimg = args.kimg
kernel.modfiles = []
kernel.moddir = None
kernel.dtb = None # TODO: fix this
arg_fail('You must specify a kernel to use.')
return kernel
def export_virtfs(qemu, arch, qemuargs, path, mount_tag, security_model='none', readonly=True) -> None:
# NB: We can't use -virtfs for this, because it can't handle a mount_tag
# that isn't a valid QEMU identifier.
fsid = 'virtfs%d' % len(qemuargs)
qemuargs.extend(['-fsdev', 'local,id=%s,path=%s,security_model=%s%s' %
(fsid, qemu.quote_optarg(path),
security_model, ',readonly' if readonly else '')])
qemuargs.extend(['-device', '%s,fsdev=%s,mount_tag=%s' % (arch.virtio_dev_type('9p'), fsid, qemu.quote_optarg(mount_tag))])
def quote_karg(arg: str) -> str:
if '"' in arg:
raise ValueError("cannot quote '\"' in kernel args")
if ' ' in arg:
return '"%s"' % arg
return arg
# Allowed characters in mount paths. We can extend this over time if needed.
_SAFE_PATH_PATTERN = '[a-zA-Z0-9_+ /.-]+'
_RWDIR_RE = re.compile('^(%s)(?:=(%s))?$' %
def do_it() -> int:
args = _ARGPARSER.parse_args()
arch = architectures.get(args.arch)
is_native = (args.arch == uname.machine)
qemu = qemu_helpers.Qemu(arch.qemuname)
need_initramfs = args.force_initramfs or qemu.cannot_overmount_virtfs
config = mkinitramfs.Config()
kernel = find_kernel_and_mods(arch, args)
config.modfiles = kernel.modfiles
if config.modfiles:
need_initramfs = True
qemuargs: List[str] = [qemu.qemubin]
kernelargs = []
# Put the '-name' flag first so it's easily visible in ps, top, etc.
kernelargs.append('virtme_hostname=%s' %
# Set up virtfs
export_virtfs(qemu, arch, qemuargs, args.root, '/dev/root', readonly=(not
guest_tools_path = resources.find_guest_tools()
if guest_tools_path is None:
raise ValueError("couldn't find guest tools -- virtme is installed incorrectly")
export_virtfs(qemu, arch, qemuargs, guest_tools_path,
initcmds = ['mkdir -p /run/virtme/guesttools',
'/bin/mount -n -t 9p -o ro,version=9p2000.L,trans=virtio,access=any virtme.guesttools /run/virtme/guesttools',
'exec /run/virtme/guesttools/virtme-init']
# Arrange for modules to end up in the right place
if kernel.moddir is not None:
if kernel.use_root_mods:
# Tell virtme-init to use the root /lib/modules
# We're grabbing modules from somewhere other than /lib/modules.
# Rather than mounting it separately, symlink it in the guest.
# This allows symlinks within the module directory to resolve
# correctly in the guest.
kernelargs.append("virtme_link_mods=/%s" % qemu.quote_optarg(os.path.relpath(kernel.moddir, args.root)))
# No modules are available. virtme-init will hide /lib/modules/KVER
# Set up mounts
mount_index = 0
for dirtype, dirarg in itertools.chain((('rwdir', i) for i in args.rwdir),
(('rodir', i) for i in args.rodir)):
m = _RWDIR_RE.match(dirarg)
if not m:
arg_fail('invalid --%s parameter %r' % (dirtype, dirarg))
if is not None:
guestpath =
hostpath =
hostpath =
guestpath = os.path.relpath(hostpath, args.root)
if guestpath.startswith('..'):
arg_fail('%r is not inside the root' % hostpath)
idx = mount_index
mount_index += 1
tag = 'virtme.initmount%d' % idx
export_virtfs(qemu, arch, qemuargs, hostpath, tag, readonly=(dirtype != 'rwdir'))
kernelargs.append('virtme_initmount%d=%s' % (idx, guestpath))
# Turn on KVM if available
if is_native:
qemuargs.extend(['-machine', 'accel=kvm:tcg'])
# Add architecture-specific options
# Set up / override baseline devices
qemuargs.extend(['-parallel', 'none'])
qemuargs.extend(['-net', 'none'])
if not and not args.script_sh and not args.script_exec:
# It would be nice to use virtconsole, but it's terminally broken
# in current kernels. Nonetheless, I'm configuring the console
# manually to make it easier to tweak in the future.
qemuargs.extend(['-echr', '1'])
qemuargs.extend(['-serial', 'none'])
qemuargs.extend(['-chardev', 'stdio,id=console,signal=off,mux=on'])
# We should be using the new-style -device serialdev,chardev=xyz,
# but many architecture-specific serial devices don't support that.
qemuargs.extend(['-serial', 'chardev:console'])
qemuargs.extend(['-mon', 'chardev=console'])
# PS/2 probing is slow; give the kernel a hint to speed it up.
# Fix the terminal defaults (and set iutf8 because that's a better
# default nowadays). I don't know of any way to keep this up to date
# after startup, though.
terminal_size = os.get_terminal_size()
kernelargs.extend(['virtme_stty_con=rows %d cols %d iutf8' %
(terminal_size.lines, terminal_size.columns)])
except OSError as e:
# don't die if running with a non-TTY stdout
if e.errno != errno.ENOTTY:
# Propagate the terminal type
if 'TERM' in os.environ:
kernelargs.extend(['TERM=%s' % os.environ['TERM']])
if args.balloon:
qemuargs.extend(['-balloon', 'virtio'])
if args.memory:
qemuargs.extend(['-m', args.memory])
if args.disk:
qemuargs.extend(['-device', '%s,id=scsi' % arch.virtio_dev_type('scsi')])
for i,d in enumerate(args.disk):
namefile = d.split('=', 1)
if len(namefile) != 2:
arg_fail('invalid argument to --disk')
name,fn = namefile
if '=' in fn or ',' in fn:
arg_fail("--disk filenames cannot contain '=' or ','")
if '=' in name or ',' in name:
arg_fail("--disk device names cannot contain '=' or ','")
driveid = 'disk%d' % i
qemuargs.extend(['-drive', 'if=none,id=%s,file=%s' % (driveid, fn),
'-device', 'scsi-hd,drive=%s,vendor=virtme,product=disk,serial=%s' % (driveid, name)])
has_script = False
def do_script(shellcmd, use_exec=False, show_boot_console=False):
arg_fail('scripts and --graphics are mutually exclusive')
nonlocal has_script
nonlocal need_initramfs
if has_script:
arg_fail('conflicting script options')
has_script = True
need_initramfs = True # TODO: Fix this
# Turn off default I/O
# Send kernel logs to stderr
qemuargs.extend(['-serial', 'none'])
qemuargs.extend(['-chardev', 'file,id=console,path=/proc/self/fd/2'])
# We should be using the new-style -device serialdev,chardev=xyz,
# but many architecture-specific serial devices don't support that.
qemuargs.extend(['-serial', 'chardev:console'])
if show_boot_console:
serdev = qemu.quote_optarg(arch.serial_dev_name(0))
kernelargs.extend(['console=%s' % serdev,
'earlyprintk=serial,%s,115200' % serdev])
# Set up a virtserialport for script I/O
qemuargs.extend(['-chardev', 'stdio,id=stdio,signal=on,mux=off'])
qemuargs.extend(['-device', arch.virtio_dev_type('serial')])
qemuargs.extend(['-device', 'virtserialport,name=virtme.scriptio,chardev=stdio'])
# Scripts shouldn't reboot
# Ask virtme-init to run the script
config.virtme_data[b'script'] = """#!/bin/sh
""".format(shellcmd=shellcmd, prefix="exec " if use_exec else "").encode('ascii')
# Nasty issue: QEMU will set O_NONBLOCK on fds 0, 1, and 2.
# This isn't inherently bad, but it can cause a problem if
# another process is reading from 1 or writing to 0, which is
# exactly what happens if you're using a terminal and you
# redirect some, but not all, of the tty fds. Work around it
# by giving QEMU private copies of the open object if either
# of them is a terminal.
for oldfd,mode in ((0,os.O_RDONLY), (1,os.O_WRONLY), (2,os.O_WRONLY)):
if os.isatty(oldfd):
newfd ='/proc/self/fd/%d' % oldfd, mode)
except OSError:
os.dup2(newfd, oldfd)
if args.script_sh is not None:
do_script(args.script_sh, show_boot_console=args.show_boot_console)
if args.script_exec is not None:
do_script(shlex.quote(args.script_exec), use_exec=True, show_boot_console=args.show_boot_console)
qemuargs.extend(['-device', 'virtio-net-pci,netdev=n0'])
if == 'user':
qemuargs.extend(['-netdev', 'user,id=n0'])
elif == 'bridge':
# This is highly experimental. At least on Fedora 30 on
# a wireless network, it appears to successfully start but
# not have any network access. Patches or guidance welcome.
# (I assume it's mostly a lost cause on a wireless network
# due to a lack of widespread or automatic WDS support.)
qemuargs.extend(['-netdev', 'bridge,id=n0,br=virbr0'])
assert False
if args.pwd:
rel_pwd = os.path.relpath(os.getcwd(), args.root)
if rel_pwd.startswith('..'):
print('current working directory is not contained in the root')
return 1
kernelargs.append('virtme_chdir=%s' % rel_pwd)
if args.cwd is not None:
if args.pwd:
arg_fail('--pwd and --cwd are mutually exclusive')
rel_cwd = os.path.relpath(args.cwd, args.root)
if rel_cwd.startswith('..'):
print('specified working directory is not contained in the root')
return 1
kernelargs.append('virtme_chdir=%s' % rel_cwd)
initrdpath: Optional[str]
if need_initramfs:
if args.busybox is not None:
config.busybox = args.busybox
busybox = mkinitramfs.find_busybox(args.root, is_native)
if busybox is None:
print('virtme-run: initramfs is needed, and no busybox was found',
return 1
config.busybox = busybox
config.access = 'rw'
# Set up the initramfs (warning: hack ahead)
tmpfd,tmpname = tempfile.mkstemp('irfs')
tmpfile = os.fdopen(tmpfd, 'r+b')
mkinitramfs.mkinitramfs(tmpfile, config)
fcntl.fcntl(tmpfd, fcntl.F_SETFD, 0)
initrdpath = '/proc/self/fd/%d' % tmpfile.fileno()
# No initramfs! Warning: this is slower than using an initramfs
# because the kernel will wait for device probing to finish.
# Sigh.
'rw' if else 'ro',
initrdpath = None
initcmds.insert(0, 'mount -t tmpfs run /run')
# Now that we're done setting up kernelargs, append user-specified args
# and then initargs
# Unknown options get turned into arguments to init, which is annoying
# because we're explicitly passing '--' to set the arguments directly.
# Fortunately, 'init=' will clear any arguments parsed so far, so make
# sure that 'init=' appears directly before '--'.
kernelargs.extend(['-c', ';'.join(initcmds)])
if args.xen is None:
# Load a normal kernel
qemuargs.extend(['-kernel', kernel.kimg])
if kernelargs:
' '.join(quote_karg(a) for a in kernelargs)])
if initrdpath is not None:
qemuargs.extend(['-initrd', initrdpath])
if kernel.dtb is not None:
qemuargs.extend(['-dtb', kernel.dtb])
# Use multiboot syntax to load Xen
qemuargs.extend(['-kernel', args.xen])
qemuargs.extend(['-initrd', '%s %s%s' % (
' '.join(quote_karg(a).replace(',', ',,') for a in kernelargs),
(',%s' % initrdpath) if initrdpath is not None else '')])
# Handle --qemu-opt(s)
if args.qemu_opts is not None:
if args.show_command:
print(' '.join(shlex.quote(a) for a in qemuargs))
# Go!
if not args.dry_run:
os.execv(qemu.qemubin, qemuargs)
return 0
def main() -> int:
return do_it()
except SilentError:
return 1
if __name__ == '__main__':
except SilentError: