blob: ad353a83f26448705c57ee051f1aabee342961dc [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
import argparse
import tempfile
import shutil
import os
import errno
import fcntl
import sys
import shlex
import re
import itertools
from .. import virtmods
from .. import modfinder
from .. import mkinitramfs
from .. import qemu_helpers
from .. import architectures
from .. import guest_tools
uname = os.uname()
def make_parser():
parser = argparse.ArgumentParser(
description='Virtualize your system (or another) under a kernel image',
)
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('-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(
title='Scripting',
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(
title='Architecture',
description="Options related to architecture selection")
g.add_argument('--arch', action='store', metavar='ARCHITECTURE',
default=uname.machine,
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 = 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):
print(message)
_ARGPARSER.print_usage()
sys.exit(1)
def find_kernel_and_mods(arch, args):
use_root_mods = False
if args.installed_kernel is not None:
kver = args.installed_kernel
modfiles = modfinder.find_modules_from_install(
virtmods.MODALIASES, kver=kver)
moddir = os.path.join('/lib/modules', kver)
kimg = '/usr/lib/modules/%s/vmlinuz' % kver
if not os.path.exists(kimg):
kimg = '/boot/vmlinuz-%s' % kver
dtb = None # For now
use_root_mods = True
elif args.kdir is not None:
kimg = os.path.join(args.kdir, arch.kimg_path())
virtme_mods = os.path.join(args.kdir, '.virtme_mods')
if os.path.exists(virtme_mods):
moddir = os.path.join(virtme_mods, 'lib/modules', '0.0.0')
modfiles = modfinder.find_modules_from_install(
virtmods.MODALIASES, root=virtme_mods, kver='0.0.0')
else:
kver = None
moddir = None
modfiles = []
dtb_path = arch.dtb_path()
if dtb_path is None:
dtb = None
else:
dtb = os.path.join(args.kdir, dtb_path)
elif args.kimg is not None:
kimg = args.kimg
modfiles = []
moddir = None
dtb = None # TODO: fix this
else:
arg_fail('You must specify a kernel to use.')
return kimg,dtb,modfiles,moddir,use_root_mods
def export_virtfs(qemu, arch, qemuargs, path, mount_tag, security_model='none', readonly=True):
# 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):
if '"' in arg:
raise ValueError("cannot quote '\"' in kernel args")
if ' ' in arg:
return '"%s"' % arg
else:
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))?$' %
(_SAFE_PATH_PATTERN, _SAFE_PATH_PATTERN))
def main():
args = _ARGPARSER.parse_args()
arch = architectures.get(args.arch)
is_native = (args.arch == uname.machine)
qemu = qemu_helpers.Qemu(arch.qemuname)
qemu.probe()
need_initramfs = args.force_initramfs or qemu.cannot_overmount_virtfs
config = mkinitramfs.Config()
kimg,dtb,modfiles,moddir,use_root_mods = find_kernel_and_mods(arch, args)
config.modfiles = modfiles
if config.modfiles:
need_initramfs = True
qemuargs = [qemu.qemubin]
kernelargs = []
# Put the '-name' flag first so it's easily visible in ps, top, etc.
if args.name:
qemuargs.extend(['-name', args.name])
kernelargs.append('virtme_hostname=%s' % args.name)
# Set up virtfs
export_virtfs(qemu, arch, qemuargs, args.root, '/dev/root', readonly=(not args.rw))
guest_tools_path = guest_tools.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,
'virtme.guesttools')
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 moddir is not None:
if use_root_mods:
# Tell virtme-init to use the root /lib/modules
kernelargs.append("virtme_root_mods=1")
else:
# 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(moddir, args.root)))
else:
# No modules are available. virtme-init will hide /lib/modules/KVER
pass
# 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 m.group(2) is not None:
guestpath = m.group(1)
hostpath = m.group(2)
else:
hostpath = m.group(1)
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
qemuargs.extend(arch.qemuargs(is_native))
# Set up / override baseline devices
qemuargs.extend(['-parallel', 'none'])
qemuargs.extend(['-net', 'none'])
if not args.graphics 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'])
kernelargs.extend(arch.earlyconsole_args())
kernelargs.extend(arch.serial_console_args())
qemuargs.extend(arch.qemu_nodisplay_args())
# PS/2 probing is slow; give the kernel a hint to speed it up.
kernelargs.extend(['psmouse.proto=exps'])
# 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.
try:
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:
raise
# 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 fn 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):
if args.graphics:
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
qemuargs.extend(arch.qemu_nodisplay_args())
# 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
qemuargs.extend(['-no-reboot'])
# Ask virtme-init to run the script
config.virtme_data[b'script'] = """#!/bin/sh
{prefix}{shellcmd}
""".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):
try:
newfd = os.open('/proc/self/fd/%d' % oldfd, mode)
except OSError:
pass
else:
os.dup2(newfd, oldfd)
os.close(newfd)
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)
if args.net:
qemuargs.extend(['-device', 'virtio-net-pci,netdev=n0'])
if args.net == 'user':
qemuargs.extend(['-netdev', 'user,id=n0'])
elif args.net == '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'])
else:
assert False
kernelargs.extend(['virtme.dhcp'])
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 need_initramfs:
if args.busybox is not None:
config.busybox = args.busybox
else:
config.busybox = mkinitramfs.find_busybox(args.root, is_native)
if config.busybox is None:
print('virtme-run: initramfs is needed, and no busybox was found',
file=sys.stderr)
return 1
if args.rw:
config.access = 'rw'
# Set up the initramfs (warning: hack ahead)
tmpfd,tmpname = tempfile.mkstemp('irfs')
os.unlink(tmpname)
tmpfile = os.fdopen(tmpfd, 'r+b')
mkinitramfs.mkinitramfs(tmpfile, config)
tmpfile.flush()
fcntl.fcntl(tmpfd, fcntl.F_SETFD, 0)
initrdpath = '/proc/self/fd/%d' % tmpfile.fileno()
else:
# No initramfs! Warning: this is slower than using an initramfs
# because the kernel will wait for device probing to finish.
# Sigh.
kernelargs.extend([
'rootfstype=9p',
'rootflags=version=9p2000.L,trans=virtio,access=any',
'raid=noautodetect',
'rw' if args.rw 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
kernelargs.extend(args.kopt)
# 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.append('init=/bin/sh')
kernelargs.append('--')
kernelargs.extend(['-c', ';'.join(initcmds)])
if args.xen is None:
# Load a normal kernel
qemuargs.extend(['-kernel', kimg])
if kernelargs:
qemuargs.extend(['-append',
' '.join(quote_karg(a) for a in kernelargs)])
if initrdpath is not None:
qemuargs.extend(['-initrd', initrdpath])
if dtb is not None:
qemuargs.extend(['-dtb', dtb])
else:
# Use multiboot syntax to load Xen
qemuargs.extend(['-kernel', args.xen])
qemuargs.extend(['-initrd', '%s %s%s' % (
kimg,
' '.join(quote_karg(a).replace(',', ',,') for a in kernelargs),
(',%s' % initrdpath) if initrdpath is not None else '')])
# Handle --qemu-opt(s)
qemuargs.extend(args.qemu_opt)
if args.qemu_opts is not None:
qemuargs.extend(args.qemu_opts)
if args.show_command:
print(' '.join(shlex.quote(a) for a in qemuargs))
# Go!
if not args.dry_run:
os.execv(qemu.qemubin, qemuargs)
if __name__ == '__main__':
exit(main())