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