qemu

FORK: QEMU emulator
git clone https://git.neptards.moe/neptards/qemu.git
Log | Files | Refs | Submodules | LICENSE

basevm.py (27620B)


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