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