xref: /qemu/tests/vm/basevm.py (revision 2e8f72ac)
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_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", 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")
309        self.ssh_port = None
310        for l in usernet_info["return"].splitlines():
311            fields = l.split()
312            if "TCP[HOST_FORWARD]" in fields and "22" in fields:
313                self.ssh_port = l.split()[3]
314        if not self.ssh_port:
315            raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
316                            usernet_info)
317
318    def console_init(self, timeout = None):
319        if timeout == None:
320            timeout = self.socket_timeout
321        vm = self._guest
322        vm.console_socket.settimeout(timeout)
323        self.console_raw_path = os.path.join(vm._temp_dir,
324                                             vm._name + "-console.raw")
325        self.console_raw_file = open(self.console_raw_path, 'wb')
326
327    def console_log(self, text):
328        for line in re.split("[\r\n]", text):
329            # filter out terminal escape sequences
330            line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
331            line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
332            # replace unprintable chars
333            line = re.sub("\x1b", "<esc>", line)
334            line = re.sub("[\x00-\x1f]", ".", line)
335            line = re.sub("[\x80-\xff]", ".", line)
336            if line == "":
337                continue
338            # log console line
339            sys.stderr.write("con recv: %s\n" % line)
340
341    def console_wait(self, expect, expectalt = None):
342        vm = self._guest
343        output = ""
344        while True:
345            try:
346                chars = vm.console_socket.recv(1)
347                if self.console_raw_file:
348                    self.console_raw_file.write(chars)
349                    self.console_raw_file.flush()
350            except socket.timeout:
351                sys.stderr.write("console: *** read timeout ***\n")
352                sys.stderr.write("console: waiting for: '%s'\n" % expect)
353                if not expectalt is None:
354                    sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
355                sys.stderr.write("console: line buffer:\n")
356                sys.stderr.write("\n")
357                self.console_log(output.rstrip())
358                sys.stderr.write("\n")
359                raise
360            output += chars.decode("latin1")
361            if expect in output:
362                break
363            if not expectalt is None and expectalt in output:
364                break
365            if "\r" in output or "\n" in output:
366                lines = re.split("[\r\n]", output)
367                output = lines.pop()
368                if self.debug:
369                    self.console_log("\n".join(lines))
370        if self.debug:
371            self.console_log(output)
372        if not expectalt is None and expectalt in output:
373            return False
374        return True
375
376    def console_consume(self):
377        vm = self._guest
378        output = ""
379        vm.console_socket.setblocking(0)
380        while True:
381            try:
382                chars = vm.console_socket.recv(1)
383            except:
384                break
385            output += chars.decode("latin1")
386            if "\r" in output or "\n" in output:
387                lines = re.split("[\r\n]", output)
388                output = lines.pop()
389                if self.debug:
390                    self.console_log("\n".join(lines))
391        if self.debug:
392            self.console_log(output)
393        vm.console_socket.setblocking(1)
394
395    def console_send(self, command):
396        vm = self._guest
397        if self.debug:
398            logline = re.sub("\n", "<enter>", command)
399            logline = re.sub("[\x00-\x1f]", ".", logline)
400            sys.stderr.write("con send: %s\n" % logline)
401        for char in list(command):
402            vm.console_socket.send(char.encode("utf-8"))
403            time.sleep(0.01)
404
405    def console_wait_send(self, wait, command):
406        self.console_wait(wait)
407        self.console_send(command)
408
409    def console_ssh_init(self, prompt, user, pw):
410        sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
411                     % self._config['ssh_pub_key'].rstrip()
412        self.console_wait_send("login:",    "%s\n" % user)
413        self.console_wait_send("Password:", "%s\n" % pw)
414        self.console_wait_send(prompt,      "mkdir .ssh\n")
415        self.console_wait_send(prompt,      sshkey_cmd)
416        self.console_wait_send(prompt,      "chmod 755 .ssh\n")
417        self.console_wait_send(prompt,      "chmod 644 .ssh/authorized_keys\n")
418
419    def console_sshd_config(self, prompt):
420        self.console_wait(prompt)
421        self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
422        for var in self.envvars:
423            self.console_wait(prompt)
424            self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
425
426    def print_step(self, text):
427        sys.stderr.write("### %s ...\n" % text)
428
429    def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
430        # Allow more time for VM to boot under TCG.
431        if not kvm_available(self.arch):
432            seconds *= self.tcg_timeout_multiplier
433        starttime = datetime.datetime.now()
434        endtime = starttime + datetime.timedelta(seconds=seconds)
435        cmd_success = False
436        while datetime.datetime.now() < endtime:
437            if wait_root and self.ssh_root(cmd) == 0:
438                cmd_success = True
439                break
440            elif self.ssh(cmd) == 0:
441                cmd_success = True
442                break
443            seconds = (endtime - datetime.datetime.now()).total_seconds()
444            logging.debug("%ds before timeout", seconds)
445            time.sleep(1)
446        if not cmd_success:
447            raise Exception("Timeout while waiting for guest ssh")
448
449    def shutdown(self):
450        self._guest.shutdown(timeout=self._shutdown_timeout)
451
452    def wait(self):
453        self._guest.wait(timeout=self._shutdown_timeout)
454
455    def graceful_shutdown(self):
456        self.ssh_root(self.poweroff)
457        self._guest.wait(timeout=self._shutdown_timeout)
458
459    def qmp(self, *args, **kwargs):
460        return self._guest.qmp(*args, **kwargs)
461
462    def gen_cloud_init_iso(self):
463        cidir = self._tmpdir
464        mdata = open(os.path.join(cidir, "meta-data"), "w")
465        name = self.name.replace(".","-")
466        mdata.writelines(["instance-id: {}-vm-0\n".format(name),
467                          "local-hostname: {}-guest\n".format(name)])
468        mdata.close()
469        udata = open(os.path.join(cidir, "user-data"), "w")
470        print("guest user:pw {}:{}".format(self._config['guest_user'],
471                                           self._config['guest_pass']))
472        udata.writelines(["#cloud-config\n",
473                          "chpasswd:\n",
474                          "  list: |\n",
475                          "    root:%s\n" % self._config['root_pass'],
476                          "    %s:%s\n" % (self._config['guest_user'],
477                                           self._config['guest_pass']),
478                          "  expire: False\n",
479                          "users:\n",
480                          "  - name: %s\n" % self._config['guest_user'],
481                          "    sudo: ALL=(ALL) NOPASSWD:ALL\n",
482                          "    ssh-authorized-keys:\n",
483                          "    - %s\n" % self._config['ssh_pub_key'],
484                          "  - name: root\n",
485                          "    ssh-authorized-keys:\n",
486                          "    - %s\n" % self._config['ssh_pub_key'],
487                          "locale: en_US.UTF-8\n"])
488        proxy = os.environ.get("http_proxy")
489        if not proxy is None:
490            udata.writelines(["apt:\n",
491                              "  proxy: %s" % proxy])
492        udata.close()
493        subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
494                               "-volid", "cidata", "-joliet", "-rock",
495                               "user-data", "meta-data"],
496                              cwd=cidir,
497                              stdin=self._devnull, stdout=self._stdout,
498                              stderr=self._stdout)
499        return os.path.join(cidir, "cloud-init.iso")
500
501def get_qemu_path(arch, build_path=None):
502    """Fetch the path to the qemu binary."""
503    # If QEMU environment variable set, it takes precedence
504    if "QEMU" in os.environ:
505        qemu_path = os.environ["QEMU"]
506    elif build_path:
507        qemu_path = os.path.join(build_path, arch + "-softmmu")
508        qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
509    else:
510        # Default is to use system path for qemu.
511        qemu_path = "qemu-system-" + arch
512    return qemu_path
513
514def get_qemu_version(qemu_path):
515    """Get the version number from the current QEMU,
516       and return the major number."""
517    output = subprocess.check_output([qemu_path, '--version'])
518    version_line = output.decode("utf-8")
519    version_num = re.split(' |\(', version_line)[3].split('.')[0]
520    return int(version_num)
521
522def parse_config(config, args):
523    """ Parse yaml config and populate our config structure.
524        The yaml config allows the user to override the
525        defaults for VM parameters.  In many cases these
526        defaults can be overridden without rebuilding the VM."""
527    if args.config:
528        config_file = args.config
529    elif 'QEMU_CONFIG' in os.environ:
530        config_file = os.environ['QEMU_CONFIG']
531    else:
532        return config
533    if not os.path.exists(config_file):
534        raise Exception("config file {} does not exist".format(config_file))
535    # We gracefully handle importing the yaml module
536    # since it might not be installed.
537    # If we are here it means the user supplied a .yml file,
538    # so if the yaml module is not installed we will exit with error.
539    try:
540        import yaml
541    except ImportError:
542        print("The python3-yaml package is needed "\
543              "to support config.yaml files")
544        # Instead of raising an exception we exit to avoid
545        # a raft of messy (expected) errors to stdout.
546        exit(1)
547    with open(config_file) as f:
548        yaml_dict = yaml.safe_load(f)
549
550    if 'qemu-conf' in yaml_dict:
551        config.update(yaml_dict['qemu-conf'])
552    else:
553        raise Exception("config file {} is not valid"\
554                        " missing qemu-conf".format(config_file))
555    return config
556
557def parse_args(vmcls):
558
559    def get_default_jobs():
560        if multiprocessing.cpu_count() > 1:
561            if kvm_available(vmcls.arch):
562                return multiprocessing.cpu_count() // 2
563            elif os.uname().machine == "x86_64" and \
564                 vmcls.arch in ["aarch64", "x86_64", "i386"]:
565                # MTTCG is available on these arches and we can allow
566                # more cores. but only up to a reasonable limit. User
567                # can always override these limits with --jobs.
568                return min(multiprocessing.cpu_count() // 2, 8)
569        else:
570            return 1
571
572    parser = argparse.ArgumentParser(
573        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
574        description="Utility for provisioning VMs and running builds",
575        epilog="""Remaining arguments are passed to the command.
576        Exit codes: 0 = success, 1 = command line error,
577        2 = environment initialization failed,
578        3 = test command failed""")
579    parser.add_argument("--debug", "-D", action="store_true",
580                        help="enable debug output")
581    parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
582                        help="image file name")
583    parser.add_argument("--force", "-f", action="store_true",
584                        help="force build image even if image exists")
585    parser.add_argument("--jobs", type=int, default=get_default_jobs(),
586                        help="number of virtual CPUs")
587    parser.add_argument("--verbose", "-V", action="store_true",
588                        help="Pass V=1 to builds within the guest")
589    parser.add_argument("--build-image", "-b", action="store_true",
590                        help="build image")
591    parser.add_argument("--build-qemu",
592                        help="build QEMU from source in guest")
593    parser.add_argument("--build-target",
594                        help="QEMU build target", default="check")
595    parser.add_argument("--build-path", default=None,
596                        help="Path of build directory, "\
597                        "for using build tree QEMU binary. ")
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
666