cachepc-qemu

Fork of AMDESE/qemu with changes for cachepc side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-qemu
Log | Files | Refs | Submodules | LICENSE | sfeed.txt

basevm.py (27512B)


      1#
      2# VM testing base class
      3#
      4# Copyright 2017-2019 Red Hat Inc.
      5#
      6# Authors:
      7#  Fam Zheng <famz@redhat.com>
      8#  Gerd Hoffmann <kraxel@redhat.com>
      9#
     10# This code is licensed under the GPL version 2 or later.  See
     11# the COPYING file in the top-level directory.
     12#
     13
     14import os
     15import re
     16import sys
     17import socket
     18import logging
     19import time
     20import datetime
     21sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python'))
     22from qemu.machine import QEMUMachine
     23from qemu.utils import get_info_usernet_hostfwd_port, kvm_available
     24import subprocess
     25import hashlib
     26import argparse
     27import atexit
     28import tempfile
     29import shutil
     30import multiprocessing
     31import traceback
     32import shlex
     33
     34SSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
     35               "..", "keys", "id_rsa")
     36SSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__),
     37                   "..", "keys", "id_rsa.pub")
     38
     39# This is the standard configuration.
     40# Any or all of these can be overridden by
     41# passing in a config argument to the VM constructor.
     42DEFAULT_CONFIG = {
     43    'cpu'             : "max",
     44    'machine'         : 'pc',
     45    'guest_user'      : "qemu",
     46    'guest_pass'      : "qemupass",
     47    'root_user'       : "root",
     48    'root_pass'       : "qemupass",
     49    'ssh_key_file'    : SSH_KEY_FILE,
     50    'ssh_pub_key_file': SSH_PUB_KEY_FILE,
     51    'memory'          : "4G",
     52    'extra_args'      : [],
     53    'qemu_args'       : "",
     54    'dns'             : "",
     55    'ssh_port'        : 0,
     56    'install_cmds'    : "",
     57    'boot_dev_type'   : "block",
     58    'ssh_timeout'     : 1,
     59}
     60BOOT_DEVICE = {
     61    'block' :  "-drive file={},if=none,id=drive0,cache=writeback "\
     62               "-device virtio-blk,drive=drive0,bootindex=0",
     63    'scsi'  :  "-device virtio-scsi-device,id=scsi "\
     64               "-drive file={},format=raw,if=none,id=hd0 "\
     65               "-device scsi-hd,drive=hd0,bootindex=0",
     66}
     67class BaseVM(object):
     68
     69    envvars = [
     70        "https_proxy",
     71        "http_proxy",
     72        "ftp_proxy",
     73        "no_proxy",
     74    ]
     75
     76    # The script to run in the guest that builds QEMU
     77    BUILD_SCRIPT = ""
     78    # The guest name, to be overridden by subclasses
     79    name = "#base"
     80    # The guest architecture, to be overridden by subclasses
     81    arch = "#arch"
     82    # command to halt the guest, can be overridden by subclasses
     83    poweroff = "poweroff"
     84    # Time to wait for shutdown to finish.
     85    shutdown_timeout_default = 30
     86    # enable IPv6 networking
     87    ipv6 = True
     88    # This is the timeout on the wait for console bytes.
     89    socket_timeout = 120
     90    # Scale up some timeouts under TCG.
     91    # 4 is arbitrary, but greater than 2,
     92    # since we found we need to wait more than twice as long.
     93    tcg_timeout_multiplier = 4
     94    def __init__(self, args, config=None):
     95        self._guest = None
     96        self._genisoimage = args.genisoimage
     97        self._build_path = args.build_path
     98        self._efi_aarch64 = args.efi_aarch64
     99        self._source_path = args.source_path
    100        # Allow input config to override defaults.
    101        self._config = DEFAULT_CONFIG.copy()
    102        if config != None:
    103            self._config.update(config)
    104        self.validate_ssh_keys()
    105        self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-",
    106                                                         suffix=".tmp",
    107                                                         dir="."))
    108        atexit.register(shutil.rmtree, self._tmpdir)
    109        # Copy the key files to a temporary directory.
    110        # Also chmod the key file to agree with ssh requirements.
    111        self._config['ssh_key'] = \
    112            open(self._config['ssh_key_file']).read().rstrip()
    113        self._config['ssh_pub_key'] = \
    114            open(self._config['ssh_pub_key_file']).read().rstrip()
    115        self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa")
    116        open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key'])
    117        subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file])
    118
    119        self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub")
    120        open(self._ssh_tmp_pub_key_file,
    121             "w").write(self._config['ssh_pub_key'])
    122
    123        self.debug = args.debug
    124        self._console_log_path = None
    125        if args.log_console:
    126                self._console_log_path = \
    127                         os.path.join(os.path.expanduser("~/.cache/qemu-vm"),
    128                                      "{}.install.log".format(self.name))
    129        self._stderr = sys.stderr
    130        self._devnull = open(os.devnull, "w")
    131        if self.debug:
    132            self._stdout = sys.stdout
    133        else:
    134            self._stdout = self._devnull
    135        netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22"
    136        self._args = [ \
    137            "-nodefaults", "-m", self._config['memory'],
    138            "-cpu", self._config['cpu'],
    139            "-netdev",
    140            netdev.format(self._config['ssh_port']) +
    141            (",ipv6=no" if not self.ipv6 else "") +
    142            (",dns=" + self._config['dns'] if self._config['dns'] else ""),
    143            "-device", "virtio-net-pci,netdev=vnet",
    144            "-vnc", "127.0.0.1:0,to=20"]
    145        if args.jobs and args.jobs > 1:
    146            self._args += ["-smp", "%d" % args.jobs]
    147        if kvm_available(self.arch):
    148            self._shutdown_timeout = self.shutdown_timeout_default
    149            self._args += ["-enable-kvm"]
    150        else:
    151            logging.info("KVM not available, not using -enable-kvm")
    152            self._shutdown_timeout = \
    153                self.shutdown_timeout_default * self.tcg_timeout_multiplier
    154        self._data_args = []
    155
    156        if self._config['qemu_args'] != None:
    157            qemu_args = self._config['qemu_args']
    158            qemu_args = qemu_args.replace('\n',' ').replace('\r','')
    159            # shlex groups quoted arguments together
    160            # we need this to keep the quoted args together for when
    161            # the QEMU command is issued later.
    162            args = shlex.split(qemu_args)
    163            self._config['extra_args'] = []
    164            for arg in args:
    165                if arg:
    166                    # Preserve quotes around arguments.
    167                    # shlex above takes them out, so add them in.
    168                    if " " in arg:
    169                        arg = '"{}"'.format(arg)
    170                    self._config['extra_args'].append(arg)
    171
    172    def validate_ssh_keys(self):
    173        """Check to see if the ssh key files exist."""
    174        if 'ssh_key_file' not in self._config or\
    175           not os.path.exists(self._config['ssh_key_file']):
    176            raise Exception("ssh key file not found.")
    177        if 'ssh_pub_key_file' not in self._config or\
    178           not os.path.exists(self._config['ssh_pub_key_file']):
    179               raise Exception("ssh pub key file not found.")
    180
    181    def wait_boot(self, wait_string=None):
    182        """Wait for the standard string we expect
    183           on completion of a normal boot.
    184           The user can also choose to override with an
    185           alternate string to wait for."""
    186        if wait_string is None:
    187            if self.login_prompt is None:
    188                raise Exception("self.login_prompt not defined")
    189            wait_string = self.login_prompt
    190        # Intentionally bump up the default timeout under TCG,
    191        # since the console wait below takes longer.
    192        timeout = self.socket_timeout
    193        if not kvm_available(self.arch):
    194            timeout *= 8
    195        self.console_init(timeout=timeout)
    196        self.console_wait(wait_string)
    197
    198    def _download_with_cache(self, url, sha256sum=None, sha512sum=None):
    199        def check_sha256sum(fname):
    200            if not sha256sum:
    201                return True
    202            checksum = subprocess.check_output(["sha256sum", fname]).split()[0]
    203            return sha256sum == checksum.decode("utf-8")
    204
    205        def check_sha512sum(fname):
    206            if not sha512sum:
    207                return True
    208            checksum = subprocess.check_output(["sha512sum", fname]).split()[0]
    209            return sha512sum == checksum.decode("utf-8")
    210
    211        cache_dir = os.path.expanduser("~/.cache/qemu-vm/download")
    212        if not os.path.exists(cache_dir):
    213            os.makedirs(cache_dir)
    214        fname = os.path.join(cache_dir,
    215                             hashlib.sha1(url.encode("utf-8")).hexdigest())
    216        if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname):
    217            return fname
    218        logging.debug("Downloading %s to %s...", url, fname)
    219        subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"],
    220                              stdout=self._stdout, stderr=self._stderr)
    221        os.rename(fname + ".download", fname)
    222        return fname
    223
    224    def _ssh_do(self, user, cmd, check):
    225        ssh_cmd = ["ssh",
    226                   "-t",
    227                   "-o", "StrictHostKeyChecking=no",
    228                   "-o", "UserKnownHostsFile=" + os.devnull,
    229                   "-o",
    230                   "ConnectTimeout={}".format(self._config["ssh_timeout"]),
    231                   "-p", str(self.ssh_port), "-i", self._ssh_tmp_key_file]
    232        # If not in debug mode, set ssh to quiet mode to
    233        # avoid printing the results of commands.
    234        if not self.debug:
    235            ssh_cmd.append("-q")
    236        for var in self.envvars:
    237            ssh_cmd += ['-o', "SendEnv=%s" % var ]
    238        assert not isinstance(cmd, str)
    239        ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
    240        logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
    241        r = subprocess.call(ssh_cmd)
    242        if check and r != 0:
    243            raise Exception("SSH command failed: %s" % cmd)
    244        return r
    245
    246    def ssh(self, *cmd):
    247        return self._ssh_do(self._config["guest_user"], cmd, False)
    248
    249    def ssh_root(self, *cmd):
    250        return self._ssh_do(self._config["root_user"], cmd, False)
    251
    252    def ssh_check(self, *cmd):
    253        self._ssh_do(self._config["guest_user"], cmd, True)
    254
    255    def ssh_root_check(self, *cmd):
    256        self._ssh_do(self._config["root_user"], cmd, True)
    257
    258    def build_image(self, img):
    259        raise NotImplementedError
    260
    261    def exec_qemu_img(self, *args):
    262        cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
    263        cmd.extend(list(args))
    264        subprocess.check_call(cmd)
    265
    266    def add_source_dir(self, src_dir):
    267        name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
    268        tarfile = os.path.join(self._tmpdir, name + ".tar")
    269        logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
    270        subprocess.check_call(["./scripts/archive-source.sh", tarfile],
    271                              cwd=src_dir, stdin=self._devnull,
    272                              stdout=self._stdout, stderr=self._stderr)
    273        self._data_args += ["-drive",
    274                            "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
    275                                    (tarfile, name),
    276                            "-device",
    277                            "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
    278
    279    def boot(self, img, extra_args=[]):
    280        boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
    281        boot_params = boot_dev.format(img)
    282        args = self._args + boot_params.split(' ')
    283        args += self._data_args + extra_args + self._config['extra_args']
    284        logging.debug("QEMU args: %s", " ".join(args))
    285        qemu_path = get_qemu_path(self.arch, self._build_path)
    286
    287        # Since console_log_path is only set when the user provides the
    288        # log_console option, we will set drain_console=True so the
    289        # console is always drained.
    290        guest = QEMUMachine(binary=qemu_path, args=args,
    291                            console_log=self._console_log_path,
    292                            drain_console=True)
    293        guest.set_machine(self._config['machine'])
    294        guest.set_console()
    295        try:
    296            guest.launch()
    297        except:
    298            logging.error("Failed to launch QEMU, command line:")
    299            logging.error(" ".join([qemu_path] + args))
    300            logging.error("Log:")
    301            logging.error(guest.get_log())
    302            logging.error("QEMU version >= 2.10 is required")
    303            raise
    304        atexit.register(self.shutdown)
    305        self._guest = guest
    306        # Init console so we can start consuming the chars.
    307        self.console_init()
    308        usernet_info = guest.qmp("human-monitor-command",
    309                                 command_line="info usernet").get("return")
    310        self.ssh_port = get_info_usernet_hostfwd_port(usernet_info)
    311        if not self.ssh_port:
    312            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
    313                            usernet_info)
    314
    315    def console_init(self, timeout = None):
    316        if timeout == None:
    317            timeout = self.socket_timeout
    318        vm = self._guest
    319        vm.console_socket.settimeout(timeout)
    320        self.console_raw_path = os.path.join(vm._temp_dir,
    321                                             vm._name + "-console.raw")
    322        self.console_raw_file = open(self.console_raw_path, 'wb')
    323
    324    def console_log(self, text):
    325        for line in re.split("[\r\n]", text):
    326            # filter out terminal escape sequences
    327            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
    328            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
    329            # replace unprintable chars
    330            line = re.sub("\x1b", "<esc>", line)
    331            line = re.sub("[\x00-\x1f]", ".", line)
    332            line = re.sub("[\x80-\xff]", ".", line)
    333            if line == "":
    334                continue
    335            # log console line
    336            sys.stderr.write("con recv: %s\n" % line)
    337
    338    def console_wait(self, expect, expectalt = None):
    339        vm = self._guest
    340        output = ""
    341        while True:
    342            try:
    343                chars = vm.console_socket.recv(1)
    344                if self.console_raw_file:
    345                    self.console_raw_file.write(chars)
    346                    self.console_raw_file.flush()
    347            except socket.timeout:
    348                sys.stderr.write("console: *** read timeout ***\n")
    349                sys.stderr.write("console: waiting for: '%s'\n" % expect)
    350                if not expectalt is None:
    351                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
    352                sys.stderr.write("console: line buffer:\n")
    353                sys.stderr.write("\n")
    354                self.console_log(output.rstrip())
    355                sys.stderr.write("\n")
    356                raise
    357            output += chars.decode("latin1")
    358            if expect in output:
    359                break
    360            if not expectalt is None and expectalt in output:
    361                break
    362            if "\r" in output or "\n" in output:
    363                lines = re.split("[\r\n]", output)
    364                output = lines.pop()
    365                if self.debug:
    366                    self.console_log("\n".join(lines))
    367        if self.debug:
    368            self.console_log(output)
    369        if not expectalt is None and expectalt in output:
    370            return False
    371        return True
    372
    373    def console_consume(self):
    374        vm = self._guest
    375        output = ""
    376        vm.console_socket.setblocking(0)
    377        while True:
    378            try:
    379                chars = vm.console_socket.recv(1)
    380            except:
    381                break
    382            output += chars.decode("latin1")
    383            if "\r" in output or "\n" in output:
    384                lines = re.split("[\r\n]", output)
    385                output = lines.pop()
    386                if self.debug:
    387                    self.console_log("\n".join(lines))
    388        if self.debug:
    389            self.console_log(output)
    390        vm.console_socket.setblocking(1)
    391
    392    def console_send(self, command):
    393        vm = self._guest
    394        if self.debug:
    395            logline = re.sub("\n", "<enter>", command)
    396            logline = re.sub("[\x00-\x1f]", ".", logline)
    397            sys.stderr.write("con send: %s\n" % logline)
    398        for char in list(command):
    399            vm.console_socket.send(char.encode("utf-8"))
    400            time.sleep(0.01)
    401
    402    def console_wait_send(self, wait, command):
    403        self.console_wait(wait)
    404        self.console_send(command)
    405
    406    def console_ssh_init(self, prompt, user, pw):
    407        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
    408                     % self._config['ssh_pub_key'].rstrip()
    409        self.console_wait_send("login:",    "%s\n" % user)
    410        self.console_wait_send("Password:", "%s\n" % pw)
    411        self.console_wait_send(prompt,      "mkdir .ssh\n")
    412        self.console_wait_send(prompt,      sshkey_cmd)
    413        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
    414        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
    415
    416    def console_sshd_config(self, prompt):
    417        self.console_wait(prompt)
    418        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
    419        for var in self.envvars:
    420            self.console_wait(prompt)
    421            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
    422
    423    def print_step(self, text):
    424        sys.stderr.write("### %s ...\n" % text)
    425
    426    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
    427        # Allow more time for VM to boot under TCG.
    428        if not kvm_available(self.arch):
    429            seconds *= self.tcg_timeout_multiplier
    430        starttime = datetime.datetime.now()
    431        endtime = starttime + datetime.timedelta(seconds=seconds)
    432        cmd_success = False
    433        while datetime.datetime.now() < endtime:
    434            if wait_root and self.ssh_root(cmd) == 0:
    435                cmd_success = True
    436                break
    437            elif self.ssh(cmd) == 0:
    438                cmd_success = True
    439                break
    440            seconds = (endtime - datetime.datetime.now()).total_seconds()
    441            logging.debug("%ds before timeout", seconds)
    442            time.sleep(1)
    443        if not cmd_success:
    444            raise Exception("Timeout while waiting for guest ssh")
    445
    446    def shutdown(self):
    447        self._guest.shutdown(timeout=self._shutdown_timeout)
    448
    449    def wait(self):
    450        self._guest.wait(timeout=self._shutdown_timeout)
    451
    452    def graceful_shutdown(self):
    453        self.ssh_root(self.poweroff)
    454        self._guest.wait(timeout=self._shutdown_timeout)
    455
    456    def qmp(self, *args, **kwargs):
    457        return self._guest.qmp(*args, **kwargs)
    458
    459    def gen_cloud_init_iso(self):
    460        cidir = self._tmpdir
    461        mdata = open(os.path.join(cidir, "meta-data"), "w")
    462        name = self.name.replace(".","-")
    463        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
    464                          "local-hostname: {}-guest\n".format(name)])
    465        mdata.close()
    466        udata = open(os.path.join(cidir, "user-data"), "w")
    467        print("guest user:pw {}:{}".format(self._config['guest_user'],
    468                                           self._config['guest_pass']))
    469        udata.writelines(["#cloud-config\n",
    470                          "chpasswd:\n",
    471                          "  list: |\n",
    472                          "    root:%s\n" % self._config['root_pass'],
    473                          "    %s:%s\n" % (self._config['guest_user'],
    474                                           self._config['guest_pass']),
    475                          "  expire: False\n",
    476                          "users:\n",
    477                          "  - name: %s\n" % self._config['guest_user'],
    478                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
    479                          "    ssh-authorized-keys:\n",
    480                          "    - %s\n" % self._config['ssh_pub_key'],
    481                          "  - name: root\n",
    482                          "    ssh-authorized-keys:\n",
    483                          "    - %s\n" % self._config['ssh_pub_key'],
    484                          "locale: en_US.UTF-8\n"])
    485        proxy = os.environ.get("http_proxy")
    486        if not proxy is None:
    487            udata.writelines(["apt:\n",
    488                              "  proxy: %s" % proxy])
    489        udata.close()
    490        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
    491                               "-volid", "cidata", "-joliet", "-rock",
    492                               "user-data", "meta-data"],
    493                              cwd=cidir,
    494                              stdin=self._devnull, stdout=self._stdout,
    495                              stderr=self._stdout)
    496        return os.path.join(cidir, "cloud-init.iso")
    497
    498def get_qemu_path(arch, build_path=None):
    499    """Fetch the path to the qemu binary."""
    500    # If QEMU environment variable set, it takes precedence
    501    if "QEMU" in os.environ:
    502        qemu_path = os.environ["QEMU"]
    503    elif build_path:
    504        qemu_path = os.path.join(build_path, arch + "-softmmu")
    505        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
    506    else:
    507        # Default is to use system path for qemu.
    508        qemu_path = "qemu-system-" + arch
    509    return qemu_path
    510
    511def get_qemu_version(qemu_path):
    512    """Get the version number from the current QEMU,
    513       and return the major number."""
    514    output = subprocess.check_output([qemu_path, '--version'])
    515    version_line = output.decode("utf-8")
    516    version_num = re.split(' |\(', version_line)[3].split('.')[0]
    517    return int(version_num)
    518
    519def parse_config(config, args):
    520    """ Parse yaml config and populate our config structure.
    521        The yaml config allows the user to override the
    522        defaults for VM parameters.  In many cases these
    523        defaults can be overridden without rebuilding the VM."""
    524    if args.config:
    525        config_file = args.config
    526    elif 'QEMU_CONFIG' in os.environ:
    527        config_file = os.environ['QEMU_CONFIG']
    528    else:
    529        return config
    530    if not os.path.exists(config_file):
    531        raise Exception("config file {} does not exist".format(config_file))
    532    # We gracefully handle importing the yaml module
    533    # since it might not be installed.
    534    # If we are here it means the user supplied a .yml file,
    535    # so if the yaml module is not installed we will exit with error.
    536    try:
    537        import yaml
    538    except ImportError:
    539        print("The python3-yaml package is needed "\
    540              "to support config.yaml files")
    541        # Instead of raising an exception we exit to avoid
    542        # a raft of messy (expected) errors to stdout.
    543        exit(1)
    544    with open(config_file) as f:
    545        yaml_dict = yaml.safe_load(f)
    546
    547    if 'qemu-conf' in yaml_dict:
    548        config.update(yaml_dict['qemu-conf'])
    549    else:
    550        raise Exception("config file {} is not valid"\
    551                        " missing qemu-conf".format(config_file))
    552    return config
    553
    554def parse_args(vmcls):
    555
    556    def get_default_jobs():
    557        if multiprocessing.cpu_count() > 1:
    558            if kvm_available(vmcls.arch):
    559                return multiprocessing.cpu_count() // 2
    560            elif os.uname().machine == "x86_64" and \
    561                 vmcls.arch in ["aarch64", "x86_64", "i386"]:
    562                # MTTCG is available on these arches and we can allow
    563                # more cores. but only up to a reasonable limit. User
    564                # can always override these limits with --jobs.
    565                return min(multiprocessing.cpu_count() // 2, 8)
    566        else:
    567            return 1
    568
    569    parser = argparse.ArgumentParser(
    570        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    571        description="Utility for provisioning VMs and running builds",
    572        epilog="""Remaining arguments are passed to the command.
    573        Exit codes: 0 = success, 1 = command line error,
    574        2 = environment initialization failed,
    575        3 = test command failed""")
    576    parser.add_argument("--debug", "-D", action="store_true",
    577                        help="enable debug output")
    578    parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
    579                        help="image file name")
    580    parser.add_argument("--force", "-f", action="store_true",
    581                        help="force build image even if image exists")
    582    parser.add_argument("--jobs", type=int, default=get_default_jobs(),
    583                        help="number of virtual CPUs")
    584    parser.add_argument("--verbose", "-V", action="store_true",
    585                        help="Pass V=1 to builds within the guest")
    586    parser.add_argument("--build-image", "-b", action="store_true",
    587                        help="build image")
    588    parser.add_argument("--build-qemu",
    589                        help="build QEMU from source in guest")
    590    parser.add_argument("--build-target",
    591                        help="QEMU build target", default="check")
    592    parser.add_argument("--build-path", default=None,
    593                        help="Path of build directory, "\
    594                        "for using build tree QEMU binary. ")
    595    parser.add_argument("--source-path", default=None,
    596                        help="Path of source directory, "\
    597                        "for finding additional files. ")
    598    parser.add_argument("--interactive", "-I", action="store_true",
    599                        help="Interactively run command")
    600    parser.add_argument("--snapshot", "-s", action="store_true",
    601                        help="run tests with a snapshot")
    602    parser.add_argument("--genisoimage", default="genisoimage",
    603                        help="iso imaging tool")
    604    parser.add_argument("--config", "-c", default=None,
    605                        help="Provide config yaml for configuration. "\
    606                        "See config_example.yaml for example.")
    607    parser.add_argument("--efi-aarch64",
    608                        default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
    609                        help="Path to efi image for aarch64 VMs.")
    610    parser.add_argument("--log-console", action="store_true",
    611                        help="Log console to file.")
    612    parser.add_argument("commands", nargs="*", help="""Remaining
    613        commands after -- are passed to command inside the VM""")
    614
    615    return parser.parse_args()
    616
    617def main(vmcls, config=None):
    618    try:
    619        if config == None:
    620            config = DEFAULT_CONFIG
    621        args = parse_args(vmcls)
    622        if not args.commands and not args.build_qemu and not args.build_image:
    623            print("Nothing to do?")
    624            return 1
    625        config = parse_config(config, args)
    626        logging.basicConfig(level=(logging.DEBUG if args.debug
    627                                   else logging.WARN))
    628        vm = vmcls(args, config=config)
    629        if args.build_image:
    630            if os.path.exists(args.image) and not args.force:
    631                sys.stderr.writelines(["Image file exists: %s\n" % args.image,
    632                                      "Use --force option to overwrite\n"])
    633                return 1
    634            return vm.build_image(args.image)
    635        if args.build_qemu:
    636            vm.add_source_dir(args.build_qemu)
    637            cmd = [vm.BUILD_SCRIPT.format(
    638                   configure_opts = " ".join(args.commands),
    639                   jobs=int(args.jobs),
    640                   target=args.build_target,
    641                   verbose = "V=1" if args.verbose else "")]
    642        else:
    643            cmd = args.commands
    644        img = args.image
    645        if args.snapshot:
    646            img += ",snapshot=on"
    647        vm.boot(img)
    648        vm.wait_ssh()
    649    except Exception as e:
    650        if isinstance(e, SystemExit) and e.code == 0:
    651            return 0
    652        sys.stderr.write("Failed to prepare guest environment\n")
    653        traceback.print_exc()
    654        return 2
    655
    656    exitcode = 0
    657    if vm.ssh(*cmd) != 0:
    658        exitcode = 3
    659    if args.interactive:
    660        vm.ssh()
    661
    662    if not args.snapshot:
    663        vm.graceful_shutdown()
    664
    665    return exitcode