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 # If not in debug mode, set ssh to quiet mode to 238 # avoid printing the results of commands. 239 if not self.debug: 240 ssh_cmd.append("-q") 241 for var in self.envvars: 242 ssh_cmd += ['-o', "SendEnv=%s" % var ] 243 assert not isinstance(cmd, str) 244 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 245 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 246 r = subprocess.call(ssh_cmd) 247 if check and r != 0: 248 raise Exception("SSH command failed: %s" % cmd) 249 return r 250 251 def ssh(self, *cmd): 252 return self._ssh_do(self._config["guest_user"], cmd, False) 253 254 def ssh_root(self, *cmd): 255 return self._ssh_do(self._config["root_user"], cmd, False) 256 257 def ssh_check(self, *cmd): 258 self._ssh_do(self._config["guest_user"], cmd, True) 259 260 def ssh_root_check(self, *cmd): 261 self._ssh_do(self._config["root_user"], cmd, True) 262 263 def build_image(self, img): 264 raise NotImplementedError 265 266 def exec_qemu_img(self, *args): 267 cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 268 cmd.extend(list(args)) 269 subprocess.check_call(cmd) 270 271 def add_source_dir(self, src_dir): 272 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 273 tarfile = os.path.join(self._tmpdir, name + ".tar") 274 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 275 subprocess.check_call(["./scripts/archive-source.sh", tarfile], 276 cwd=src_dir, stdin=self._devnull, 277 stdout=self._stdout, stderr=self._stderr) 278 self._data_args += ["-drive", 279 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 280 (tarfile, name), 281 "-device", 282 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 283 284 def boot(self, img, extra_args=[]): 285 boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 286 boot_params = boot_dev.format(img) 287 args = self._args + boot_params.split(' ') 288 args += self._data_args + extra_args + self._config['extra_args'] 289 logging.debug("QEMU args: %s", " ".join(args)) 290 qemu_path = get_qemu_path(self.arch, self._build_path) 291 292 # Since console_log_path is only set when the user provides the 293 # log_console option, we will set drain_console=True so the 294 # console is always drained. 295 guest = QEMUMachine(binary=qemu_path, args=args, 296 console_log=self._console_log_path, 297 drain_console=True) 298 guest.set_machine(self._config['machine']) 299 guest.set_console() 300 try: 301 guest.launch() 302 except: 303 logging.error("Failed to launch QEMU, command line:") 304 logging.error(" ".join([qemu_path] + args)) 305 logging.error("Log:") 306 logging.error(guest.get_log()) 307 logging.error("QEMU version >= 2.10 is required") 308 raise 309 atexit.register(self.shutdown) 310 self._guest = guest 311 # Init console so we can start consuming the chars. 312 self.console_init() 313 usernet_info = guest.qmp("human-monitor-command", 314 command_line="info usernet").get("return") 315 self.ssh_port = get_info_usernet_hostfwd_port(usernet_info) 316 if not self.ssh_port: 317 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 318 usernet_info) 319 320 def console_init(self, timeout = None): 321 if timeout == None: 322 timeout = self.socket_timeout 323 vm = self._guest 324 vm.console_socket.settimeout(timeout) 325 self.console_raw_path = os.path.join(vm._temp_dir, 326 vm._name + "-console.raw") 327 self.console_raw_file = open(self.console_raw_path, 'wb') 328 329 def console_log(self, text): 330 for line in re.split("[\r\n]", text): 331 # filter out terminal escape sequences 332 line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line) 333 line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line) 334 # replace unprintable chars 335 line = re.sub("\x1b", "<esc>", line) 336 line = re.sub("[\x00-\x1f]", ".", line) 337 line = re.sub("[\x80-\xff]", ".", line) 338 if line == "": 339 continue 340 # log console line 341 sys.stderr.write("con recv: %s\n" % line) 342 343 def console_wait(self, expect, expectalt = None): 344 vm = self._guest 345 output = "" 346 while True: 347 try: 348 chars = vm.console_socket.recv(1) 349 if self.console_raw_file: 350 self.console_raw_file.write(chars) 351 self.console_raw_file.flush() 352 except socket.timeout: 353 sys.stderr.write("console: *** read timeout ***\n") 354 sys.stderr.write("console: waiting for: '%s'\n" % expect) 355 if not expectalt is None: 356 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 357 sys.stderr.write("console: line buffer:\n") 358 sys.stderr.write("\n") 359 self.console_log(output.rstrip()) 360 sys.stderr.write("\n") 361 raise 362 output += chars.decode("latin1") 363 if expect in output: 364 break 365 if not expectalt is None and expectalt in output: 366 break 367 if "\r" in output or "\n" in output: 368 lines = re.split("[\r\n]", output) 369 output = lines.pop() 370 if self.debug: 371 self.console_log("\n".join(lines)) 372 if self.debug: 373 self.console_log(output) 374 if not expectalt is None and expectalt in output: 375 return False 376 return True 377 378 def console_consume(self): 379 vm = self._guest 380 output = "" 381 vm.console_socket.setblocking(0) 382 while True: 383 try: 384 chars = vm.console_socket.recv(1) 385 except: 386 break 387 output += chars.decode("latin1") 388 if "\r" in output or "\n" in output: 389 lines = re.split("[\r\n]", output) 390 output = lines.pop() 391 if self.debug: 392 self.console_log("\n".join(lines)) 393 if self.debug: 394 self.console_log(output) 395 vm.console_socket.setblocking(1) 396 397 def console_send(self, command): 398 vm = self._guest 399 if self.debug: 400 logline = re.sub("\n", "<enter>", command) 401 logline = re.sub("[\x00-\x1f]", ".", logline) 402 sys.stderr.write("con send: %s\n" % logline) 403 for char in list(command): 404 vm.console_socket.send(char.encode("utf-8")) 405 time.sleep(0.01) 406 407 def console_wait_send(self, wait, command): 408 self.console_wait(wait) 409 self.console_send(command) 410 411 def console_ssh_init(self, prompt, user, pw): 412 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 413 % self._config['ssh_pub_key'].rstrip() 414 self.console_wait_send("login:", "%s\n" % user) 415 self.console_wait_send("Password:", "%s\n" % pw) 416 self.console_wait_send(prompt, "mkdir .ssh\n") 417 self.console_wait_send(prompt, sshkey_cmd) 418 self.console_wait_send(prompt, "chmod 755 .ssh\n") 419 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 420 421 def console_sshd_config(self, prompt): 422 self.console_wait(prompt) 423 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 424 for var in self.envvars: 425 self.console_wait(prompt) 426 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 427 428 def print_step(self, text): 429 sys.stderr.write("### %s ...\n" % text) 430 431 def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 432 # Allow more time for VM to boot under TCG. 433 if not kvm_available(self.arch): 434 seconds *= self.tcg_timeout_multiplier 435 starttime = datetime.datetime.now() 436 endtime = starttime + datetime.timedelta(seconds=seconds) 437 cmd_success = False 438 while datetime.datetime.now() < endtime: 439 if wait_root and self.ssh_root(cmd) == 0: 440 cmd_success = True 441 break 442 elif self.ssh(cmd) == 0: 443 cmd_success = True 444 break 445 seconds = (endtime - datetime.datetime.now()).total_seconds() 446 logging.debug("%ds before timeout", seconds) 447 time.sleep(1) 448 if not cmd_success: 449 raise Exception("Timeout while waiting for guest ssh") 450 451 def shutdown(self): 452 self._guest.shutdown(timeout=self._shutdown_timeout) 453 454 def wait(self): 455 self._guest.wait(timeout=self._shutdown_timeout) 456 457 def graceful_shutdown(self): 458 self.ssh_root(self.poweroff) 459 self._guest.wait(timeout=self._shutdown_timeout) 460 461 def qmp(self, *args, **kwargs): 462 return self._guest.qmp(*args, **kwargs) 463 464 def gen_cloud_init_iso(self): 465 cidir = self._tmpdir 466 mdata = open(os.path.join(cidir, "meta-data"), "w") 467 name = self.name.replace(".","-") 468 mdata.writelines(["instance-id: {}-vm-0\n".format(name), 469 "local-hostname: {}-guest\n".format(name)]) 470 mdata.close() 471 udata = open(os.path.join(cidir, "user-data"), "w") 472 print("guest user:pw {}:{}".format(self._config['guest_user'], 473 self._config['guest_pass'])) 474 udata.writelines(["#cloud-config\n", 475 "chpasswd:\n", 476 " list: |\n", 477 " root:%s\n" % self._config['root_pass'], 478 " %s:%s\n" % (self._config['guest_user'], 479 self._config['guest_pass']), 480 " expire: False\n", 481 "users:\n", 482 " - name: %s\n" % self._config['guest_user'], 483 " sudo: ALL=(ALL) NOPASSWD:ALL\n", 484 " ssh-authorized-keys:\n", 485 " - %s\n" % self._config['ssh_pub_key'], 486 " - name: root\n", 487 " ssh-authorized-keys:\n", 488 " - %s\n" % self._config['ssh_pub_key'], 489 "locale: en_US.UTF-8\n"]) 490 proxy = os.environ.get("http_proxy") 491 if not proxy is None: 492 udata.writelines(["apt:\n", 493 " proxy: %s" % proxy]) 494 udata.close() 495 subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 496 "-volid", "cidata", "-joliet", "-rock", 497 "user-data", "meta-data"], 498 cwd=cidir, 499 stdin=self._devnull, stdout=self._stdout, 500 stderr=self._stdout) 501 return os.path.join(cidir, "cloud-init.iso") 502 503def get_qemu_path(arch, build_path=None): 504 """Fetch the path to the qemu binary.""" 505 # If QEMU environment variable set, it takes precedence 506 if "QEMU" in os.environ: 507 qemu_path = os.environ["QEMU"] 508 elif build_path: 509 qemu_path = os.path.join(build_path, arch + "-softmmu") 510 qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 511 else: 512 # Default is to use system path for qemu. 513 qemu_path = "qemu-system-" + arch 514 return qemu_path 515 516def get_qemu_version(qemu_path): 517 """Get the version number from the current QEMU, 518 and return the major number.""" 519 output = subprocess.check_output([qemu_path, '--version']) 520 version_line = output.decode("utf-8") 521 version_num = re.split(' |\(', version_line)[3].split('.')[0] 522 return int(version_num) 523 524def parse_config(config, args): 525 """ Parse yaml config and populate our config structure. 526 The yaml config allows the user to override the 527 defaults for VM parameters. In many cases these 528 defaults can be overridden without rebuilding the VM.""" 529 if args.config: 530 config_file = args.config 531 elif 'QEMU_CONFIG' in os.environ: 532 config_file = os.environ['QEMU_CONFIG'] 533 else: 534 return config 535 if not os.path.exists(config_file): 536 raise Exception("config file {} does not exist".format(config_file)) 537 # We gracefully handle importing the yaml module 538 # since it might not be installed. 539 # If we are here it means the user supplied a .yml file, 540 # so if the yaml module is not installed we will exit with error. 541 try: 542 import yaml 543 except ImportError: 544 print("The python3-yaml package is needed "\ 545 "to support config.yaml files") 546 # Instead of raising an exception we exit to avoid 547 # a raft of messy (expected) errors to stdout. 548 exit(1) 549 with open(config_file) as f: 550 yaml_dict = yaml.safe_load(f) 551 552 if 'qemu-conf' in yaml_dict: 553 config.update(yaml_dict['qemu-conf']) 554 else: 555 raise Exception("config file {} is not valid"\ 556 " missing qemu-conf".format(config_file)) 557 return config 558 559def parse_args(vmcls): 560 561 def get_default_jobs(): 562 if multiprocessing.cpu_count() > 1: 563 if kvm_available(vmcls.arch): 564 return multiprocessing.cpu_count() // 2 565 elif os.uname().machine == "x86_64" and \ 566 vmcls.arch in ["aarch64", "x86_64", "i386"]: 567 # MTTCG is available on these arches and we can allow 568 # more cores. but only up to a reasonable limit. User 569 # can always override these limits with --jobs. 570 return min(multiprocessing.cpu_count() // 2, 8) 571 else: 572 return 1 573 574 parser = argparse.ArgumentParser( 575 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 576 description="Utility for provisioning VMs and running builds", 577 epilog="""Remaining arguments are passed to the command. 578 Exit codes: 0 = success, 1 = command line error, 579 2 = environment initialization failed, 580 3 = test command failed""") 581 parser.add_argument("--debug", "-D", action="store_true", 582 help="enable debug output") 583 parser.add_argument("--image", "-i", default="%s.img" % vmcls.name, 584 help="image file name") 585 parser.add_argument("--force", "-f", action="store_true", 586 help="force build image even if image exists") 587 parser.add_argument("--jobs", type=int, default=get_default_jobs(), 588 help="number of virtual CPUs") 589 parser.add_argument("--verbose", "-V", action="store_true", 590 help="Pass V=1 to builds within the guest") 591 parser.add_argument("--build-image", "-b", action="store_true", 592 help="build image") 593 parser.add_argument("--build-qemu", 594 help="build QEMU from source in guest") 595 parser.add_argument("--build-target", 596 help="QEMU build target", default="check") 597 parser.add_argument("--build-path", default=None, 598 help="Path of build directory, "\ 599 "for using build tree QEMU binary. ") 600 parser.add_argument("--source-path", default=None, 601 help="Path of source directory, "\ 602 "for finding additional files. ") 603 parser.add_argument("--interactive", "-I", action="store_true", 604 help="Interactively run command") 605 parser.add_argument("--snapshot", "-s", action="store_true", 606 help="run tests with a snapshot") 607 parser.add_argument("--genisoimage", default="genisoimage", 608 help="iso imaging tool") 609 parser.add_argument("--config", "-c", default=None, 610 help="Provide config yaml for configuration. "\ 611 "See config_example.yaml for example.") 612 parser.add_argument("--efi-aarch64", 613 default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 614 help="Path to efi image for aarch64 VMs.") 615 parser.add_argument("--log-console", action="store_true", 616 help="Log console to file.") 617 parser.add_argument("commands", nargs="*", help="""Remaining 618 commands after -- are passed to command inside the VM""") 619 620 return parser.parse_args() 621 622def main(vmcls, config=None): 623 try: 624 if config == None: 625 config = DEFAULT_CONFIG 626 args = parse_args(vmcls) 627 if not args.commands and not args.build_qemu and not args.build_image: 628 print("Nothing to do?") 629 return 1 630 config = parse_config(config, args) 631 logging.basicConfig(level=(logging.DEBUG if args.debug 632 else logging.WARN)) 633 vm = vmcls(args, config=config) 634 if args.build_image: 635 if os.path.exists(args.image) and not args.force: 636 sys.stderr.writelines(["Image file exists: %s\n" % args.image, 637 "Use --force option to overwrite\n"]) 638 return 1 639 return vm.build_image(args.image) 640 if args.build_qemu: 641 vm.add_source_dir(args.build_qemu) 642 cmd = [vm.BUILD_SCRIPT.format( 643 configure_opts = " ".join(args.commands), 644 jobs=int(args.jobs), 645 target=args.build_target, 646 verbose = "V=1" if args.verbose else "")] 647 else: 648 cmd = args.commands 649 img = args.image 650 if args.snapshot: 651 img += ",snapshot=on" 652 vm.boot(img) 653 vm.wait_ssh() 654 except Exception as e: 655 if isinstance(e, SystemExit) and e.code == 0: 656 return 0 657 sys.stderr.write("Failed to prepare guest environment\n") 658 traceback.print_exc() 659 return 2 660 661 exitcode = 0 662 if vm.ssh(*cmd) != 0: 663 exitcode = 3 664 if args.interactive: 665 vm.ssh() 666 667 if not args.snapshot: 668 vm.graceful_shutdown() 669 670 return exitcode 671