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 21import subprocess 22import hashlib 23import argparse 24import atexit 25import tempfile 26import shutil 27import multiprocessing 28import traceback 29import shlex 30 31from qemu.machine import QEMUMachine 32from qemu.utils import get_info_usernet_hostfwd_port, kvm_available 33 34SSH_KEY_FILE = os.path.join(os.path.dirname(__file__), 35 "..", "keys", "id_rsa") 36SSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__), 37 "..", "keys", "id_rsa.pub") 38 39# This is the standard configuration. 40# Any or all of these can be overridden by 41# passing in a config argument to the VM constructor. 42DEFAULT_CONFIG = { 43 'cpu' : "max", 44 'machine' : 'pc', 45 'guest_user' : "qemu", 46 'guest_pass' : "qemupass", 47 'root_user' : "root", 48 'root_pass' : "qemupass", 49 'ssh_key_file' : SSH_KEY_FILE, 50 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 51 'memory' : "4G", 52 'extra_args' : [], 53 'qemu_args' : "", 54 'dns' : "", 55 'ssh_port' : 0, 56 'install_cmds' : "", 57 'boot_dev_type' : "block", 58 'ssh_timeout' : 1, 59} 60BOOT_DEVICE = { 61 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 62 "-device virtio-blk,drive=drive0,bootindex=0", 63 'scsi' : "-device virtio-scsi-device,id=scsi "\ 64 "-drive file={},format=raw,if=none,id=hd0 "\ 65 "-device scsi-hd,drive=hd0,bootindex=0", 66} 67class BaseVM(object): 68 69 envvars = [ 70 "https_proxy", 71 "http_proxy", 72 "ftp_proxy", 73 "no_proxy", 74 ] 75 76 # The script to run in the guest that builds QEMU 77 BUILD_SCRIPT = "" 78 # The guest name, to be overridden by subclasses 79 name = "#base" 80 # The guest architecture, to be overridden by subclasses 81 arch = "#arch" 82 # command to halt the guest, can be overridden by subclasses 83 poweroff = "poweroff" 84 # Time to wait for shutdown to finish. 85 shutdown_timeout_default = 30 86 # enable IPv6 networking 87 ipv6 = True 88 # This is the timeout on the wait for console bytes. 89 socket_timeout = 120 90 # Scale up some timeouts under TCG. 91 # 4 is arbitrary, but greater than 2, 92 # since we found we need to wait more than twice as long. 93 tcg_timeout_multiplier = 4 94 def __init__(self, args, config=None): 95 self._guest = None 96 self._genisoimage = args.genisoimage 97 self._build_path = args.build_path 98 self._efi_aarch64 = args.efi_aarch64 99 self._source_path = args.source_path 100 # Allow input config to override defaults. 101 self._config = DEFAULT_CONFIG.copy() 102 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 504def 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 517def 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 525def 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 560def 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 623def 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 672