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