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