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 30import json 31 32from qemu.machine import QEMUMachine 33from qemu.utils import get_info_usernet_hostfwd_port, kvm_available 34 35SSH_KEY_FILE = os.path.join(os.path.dirname(__file__), 36 "..", "keys", "id_rsa") 37SSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__), 38 "..", "keys", "id_rsa.pub") 39 40# This is the standard configuration. 41# Any or all of these can be overridden by 42# passing in a config argument to the VM constructor. 43DEFAULT_CONFIG = { 44 'cpu' : "max", 45 'machine' : 'pc', 46 'guest_user' : "qemu", 47 'guest_pass' : "qemupass", 48 'root_user' : "root", 49 'root_pass' : "qemupass", 50 'ssh_key_file' : SSH_KEY_FILE, 51 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 52 'memory' : "4G", 53 'extra_args' : [], 54 'qemu_args' : "", 55 'dns' : "", 56 'ssh_port' : 0, 57 'install_cmds' : "", 58 'boot_dev_type' : "block", 59 'ssh_timeout' : 1, 60} 61BOOT_DEVICE = { 62 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 63 "-device virtio-blk,drive=drive0,bootindex=0", 64 'scsi' : "-device virtio-scsi-device,id=scsi "\ 65 "-drive file={},format=raw,if=none,id=hd0 "\ 66 "-device scsi-hd,drive=hd0,bootindex=0", 67} 68class BaseVM(object): 69 70 envvars = [ 71 "https_proxy", 72 "http_proxy", 73 "ftp_proxy", 74 "no_proxy", 75 ] 76 77 # The script to run in the guest that builds QEMU 78 BUILD_SCRIPT = "" 79 # The guest name, to be overridden by subclasses 80 name = "#base" 81 # The guest architecture, to be overridden by subclasses 82 arch = "#arch" 83 # command to halt the guest, can be overridden by subclasses 84 poweroff = "poweroff" 85 # Time to wait for shutdown to finish. 86 shutdown_timeout_default = 30 87 # enable IPv6 networking 88 ipv6 = True 89 # This is the timeout on the wait for console bytes. 90 socket_timeout = 120 91 # Scale up some timeouts under TCG. 92 # 4 is arbitrary, but greater than 2, 93 # since we found we need to wait more than twice as long. 94 tcg_timeout_multiplier = 4 95 def __init__(self, args, config=None): 96 self._guest = None 97 self._genisoimage = args.genisoimage 98 self._build_path = args.build_path 99 self._efi_aarch64 = args.efi_aarch64 100 self._source_path = args.source_path 101 # Allow input config to override defaults. 102 self._config = DEFAULT_CONFIG.copy() 103 104 # 1GB per core, minimum of 4. This is only a default. 105 mem = max(4, args.jobs) 106 self._config['memory'] = f"{mem}G" 107 108 if config != None: 109 self._config.update(config) 110 self.validate_ssh_keys() 111 self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 112 suffix=".tmp", 113 dir=".")) 114 atexit.register(shutil.rmtree, self._tmpdir) 115 # Copy the key files to a temporary directory. 116 # Also chmod the key file to agree with ssh requirements. 117 self._config['ssh_key'] = \ 118 open(self._config['ssh_key_file']).read().rstrip() 119 self._config['ssh_pub_key'] = \ 120 open(self._config['ssh_pub_key_file']).read().rstrip() 121 self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa") 122 open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key']) 123 subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file]) 124 125 self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 126 open(self._ssh_tmp_pub_key_file, 127 "w").write(self._config['ssh_pub_key']) 128 129 self.debug = args.debug 130 self._console_log_path = None 131 if args.log_console: 132 self._console_log_path = \ 133 os.path.join(os.path.expanduser("~/.cache/qemu-vm"), 134 "{}.install.log".format(self.name)) 135 self._stderr = sys.stderr 136 self._devnull = open(os.devnull, "w") 137 if self.debug: 138 self._stdout = sys.stdout 139 else: 140 self._stdout = self._devnull 141 netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22" 142 self._args = [ \ 143 "-nodefaults", "-m", self._config['memory'], 144 "-cpu", self._config['cpu'], 145 "-netdev", 146 netdev.format(self._config['ssh_port']) + 147 (",ipv6=no" if not self.ipv6 else "") + 148 (",dns=" + self._config['dns'] if self._config['dns'] else ""), 149 "-device", "virtio-net-pci,netdev=vnet", 150 "-vnc", "127.0.0.1:0,to=20"] 151 if args.jobs and args.jobs > 1: 152 self._args += ["-smp", "%d" % args.jobs] 153 if kvm_available(self.arch): 154 self._shutdown_timeout = self.shutdown_timeout_default 155 self._args += ["-enable-kvm"] 156 else: 157 logging.info("KVM not available, not using -enable-kvm") 158 self._shutdown_timeout = \ 159 self.shutdown_timeout_default * self.tcg_timeout_multiplier 160 self._data_args = [] 161 162 if self._config['qemu_args'] != None: 163 qemu_args = self._config['qemu_args'] 164 qemu_args = qemu_args.replace('\n',' ').replace('\r','') 165 # shlex groups quoted arguments together 166 # we need this to keep the quoted args together for when 167 # the QEMU command is issued later. 168 args = shlex.split(qemu_args) 169 self._config['extra_args'] = [] 170 for arg in args: 171 if arg: 172 # Preserve quotes around arguments. 173 # shlex above takes them out, so add them in. 174 if " " in arg: 175 arg = '"{}"'.format(arg) 176 self._config['extra_args'].append(arg) 177 178 def validate_ssh_keys(self): 179 """Check to see if the ssh key files exist.""" 180 if 'ssh_key_file' not in self._config or\ 181 not os.path.exists(self._config['ssh_key_file']): 182 raise Exception("ssh key file not found.") 183 if 'ssh_pub_key_file' not in self._config or\ 184 not os.path.exists(self._config['ssh_pub_key_file']): 185 raise Exception("ssh pub key file not found.") 186 187 def wait_boot(self, wait_string=None): 188 """Wait for the standard string we expect 189 on completion of a normal boot. 190 The user can also choose to override with an 191 alternate string to wait for.""" 192 if wait_string is None: 193 if self.login_prompt is None: 194 raise Exception("self.login_prompt not defined") 195 wait_string = self.login_prompt 196 # Intentionally bump up the default timeout under TCG, 197 # since the console wait below takes longer. 198 timeout = self.socket_timeout 199 if not kvm_available(self.arch): 200 timeout *= 8 201 self.console_init(timeout=timeout) 202 self.console_wait(wait_string) 203 204 def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 205 def check_sha256sum(fname): 206 if not sha256sum: 207 return True 208 checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 209 return sha256sum == checksum.decode("utf-8") 210 211 def check_sha512sum(fname): 212 if not sha512sum: 213 return True 214 checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 215 return sha512sum == checksum.decode("utf-8") 216 217 cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 218 if not os.path.exists(cache_dir): 219 os.makedirs(cache_dir) 220 fname = os.path.join(cache_dir, 221 hashlib.sha1(url.encode("utf-8")).hexdigest()) 222 if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 223 return fname 224 logging.debug("Downloading %s to %s...", url, fname) 225 subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 226 stdout=self._stdout, stderr=self._stderr) 227 os.rename(fname + ".download", fname) 228 return fname 229 230 def _ssh_do(self, user, cmd, check): 231 ssh_cmd = ["ssh", 232 "-t", 233 "-o", "StrictHostKeyChecking=no", 234 "-o", "UserKnownHostsFile=" + os.devnull, 235 "-o", 236 "ConnectTimeout={}".format(self._config["ssh_timeout"]), 237 "-p", str(self.ssh_port), "-i", self._ssh_tmp_key_file, 238 "-o", "IdentitiesOnly=yes"] 239 # If not in debug mode, set ssh to quiet mode to 240 # avoid printing the results of commands. 241 if not self.debug: 242 ssh_cmd.append("-q") 243 for var in self.envvars: 244 ssh_cmd += ['-o', "SendEnv=%s" % var ] 245 assert not isinstance(cmd, str) 246 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 247 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 248 r = subprocess.call(ssh_cmd) 249 if check and r != 0: 250 raise Exception("SSH command failed: %s" % cmd) 251 return r 252 253 def ssh(self, *cmd): 254 return self._ssh_do(self._config["guest_user"], cmd, False) 255 256 def ssh_root(self, *cmd): 257 return self._ssh_do(self._config["root_user"], cmd, False) 258 259 def ssh_check(self, *cmd): 260 self._ssh_do(self._config["guest_user"], cmd, True) 261 262 def ssh_root_check(self, *cmd): 263 self._ssh_do(self._config["root_user"], cmd, True) 264 265 def build_image(self, img): 266 raise NotImplementedError 267 268 def exec_qemu_img(self, *args): 269 cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 270 cmd.extend(list(args)) 271 subprocess.check_call(cmd) 272 273 def add_source_dir(self, src_dir): 274 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 275 tarfile = os.path.join(self._tmpdir, name + ".tar") 276 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 277 subprocess.check_call(["./scripts/archive-source.sh", tarfile], 278 cwd=src_dir, stdin=self._devnull, 279 stdout=self._stdout, stderr=self._stderr) 280 self._data_args += ["-drive", 281 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 282 (tarfile, name), 283 "-device", 284 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 285 286 def boot(self, img, extra_args=[]): 287 boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 288 boot_params = boot_dev.format(img) 289 args = self._args + boot_params.split(' ') 290 args += self._data_args + extra_args + self._config['extra_args'] 291 logging.debug("QEMU args: %s", " ".join(args)) 292 qemu_path = get_qemu_path(self.arch, self._build_path) 293 294 # Since console_log_path is only set when the user provides the 295 # log_console option, we will set drain_console=True so the 296 # console is always drained. 297 guest = QEMUMachine(binary=qemu_path, args=args, 298 console_log=self._console_log_path, 299 drain_console=True) 300 guest.set_machine(self._config['machine']) 301 guest.set_console() 302 try: 303 guest.launch() 304 except: 305 logging.error("Failed to launch QEMU, command line:") 306 logging.error(" ".join([qemu_path] + args)) 307 logging.error("Log:") 308 logging.error(guest.get_log()) 309 logging.error("QEMU version >= 2.10 is required") 310 raise 311 atexit.register(self.shutdown) 312 self._guest = guest 313 # Init console so we can start consuming the chars. 314 self.console_init() 315 usernet_info = guest.cmd("human-monitor-command", 316 command_line="info usernet") 317 self.ssh_port = get_info_usernet_hostfwd_port(usernet_info) 318 if not self.ssh_port: 319 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 320 usernet_info) 321 322 def console_init(self, timeout = None): 323 if timeout == None: 324 timeout = self.socket_timeout 325 vm = self._guest 326 vm.console_socket.settimeout(timeout) 327 self.console_raw_path = os.path.join(vm._temp_dir, 328 vm._name + "-console.raw") 329 self.console_raw_file = open(self.console_raw_path, 'wb') 330 331 def console_log(self, text): 332 for line in re.split("[\r\n]", text): 333 # filter out terminal escape sequences 334 line = re.sub("\x1b\\[[0-9;?]*[a-zA-Z]", "", line) 335 line = re.sub("\x1b\\([0-9;?]*[a-zA-Z]", "", line) 336 # replace unprintable chars 337 line = re.sub("\x1b", "<esc>", line) 338 line = re.sub("[\x00-\x1f]", ".", line) 339 line = re.sub("[\x80-\xff]", ".", line) 340 if line == "": 341 continue 342 # log console line 343 sys.stderr.write("con recv: %s\n" % line) 344 345 def console_wait(self, expect, expectalt = None): 346 vm = self._guest 347 output = "" 348 while True: 349 try: 350 chars = vm.console_socket.recv(1) 351 if self.console_raw_file: 352 self.console_raw_file.write(chars) 353 self.console_raw_file.flush() 354 except socket.timeout: 355 sys.stderr.write("console: *** read timeout ***\n") 356 sys.stderr.write("console: waiting for: '%s'\n" % expect) 357 if not expectalt is None: 358 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 359 sys.stderr.write("console: line buffer:\n") 360 sys.stderr.write("\n") 361 self.console_log(output.rstrip()) 362 sys.stderr.write("\n") 363 raise 364 output += chars.decode("latin1") 365 if expect in output: 366 break 367 if not expectalt is None and expectalt in output: 368 break 369 if "\r" in output or "\n" in output: 370 lines = re.split("[\r\n]", output) 371 output = lines.pop() 372 if self.debug: 373 self.console_log("\n".join(lines)) 374 if self.debug: 375 self.console_log(output) 376 if not expectalt is None and expectalt in output: 377 return False 378 return True 379 380 def console_consume(self): 381 vm = self._guest 382 output = "" 383 vm.console_socket.setblocking(0) 384 while True: 385 try: 386 chars = vm.console_socket.recv(1) 387 except: 388 break 389 output += chars.decode("latin1") 390 if "\r" in output or "\n" in output: 391 lines = re.split("[\r\n]", output) 392 output = lines.pop() 393 if self.debug: 394 self.console_log("\n".join(lines)) 395 if self.debug: 396 self.console_log(output) 397 vm.console_socket.setblocking(1) 398 399 def console_send(self, command): 400 vm = self._guest 401 if self.debug: 402 logline = re.sub("\n", "<enter>", command) 403 logline = re.sub("[\x00-\x1f]", ".", logline) 404 sys.stderr.write("con send: %s\n" % logline) 405 for char in list(command): 406 vm.console_socket.send(char.encode("utf-8")) 407 time.sleep(0.01) 408 409 def console_wait_send(self, wait, command): 410 self.console_wait(wait) 411 self.console_send(command) 412 413 def console_ssh_init(self, prompt, user, pw): 414 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 415 % self._config['ssh_pub_key'].rstrip() 416 self.console_wait_send("login:", "%s\n" % user) 417 self.console_wait_send("Password:", "%s\n" % pw) 418 self.console_wait_send(prompt, "mkdir .ssh\n") 419 self.console_wait_send(prompt, sshkey_cmd) 420 self.console_wait_send(prompt, "chmod 755 .ssh\n") 421 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 422 423 def console_sshd_config(self, prompt): 424 self.console_wait(prompt) 425 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 426 self.console_wait(prompt) 427 self.console_send("echo 'UseDNS no' >> /etc/ssh/sshd_config\n") 428 for var in self.envvars: 429 self.console_wait(prompt) 430 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 431 432 def print_step(self, text): 433 sys.stderr.write("### %s ...\n" % text) 434 435 def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 436 # Allow more time for VM to boot under TCG. 437 if not kvm_available(self.arch): 438 seconds *= self.tcg_timeout_multiplier 439 starttime = datetime.datetime.now() 440 endtime = starttime + datetime.timedelta(seconds=seconds) 441 cmd_success = False 442 while datetime.datetime.now() < endtime: 443 if wait_root and self.ssh_root(cmd) == 0: 444 cmd_success = True 445 break 446 elif self.ssh(cmd) == 0: 447 cmd_success = True 448 break 449 seconds = (endtime - datetime.datetime.now()).total_seconds() 450 logging.debug("%ds before timeout", seconds) 451 time.sleep(1) 452 if not cmd_success: 453 raise Exception("Timeout while waiting for guest ssh") 454 455 def shutdown(self): 456 self._guest.shutdown(timeout=self._shutdown_timeout) 457 458 def wait(self): 459 self._guest.wait(timeout=self._shutdown_timeout) 460 461 def graceful_shutdown(self): 462 self.ssh_root(self.poweroff) 463 self._guest.wait(timeout=self._shutdown_timeout) 464 465 def qmp(self, *args, **kwargs): 466 return self._guest.qmp(*args, **kwargs) 467 468 def gen_cloud_init_iso(self): 469 cidir = self._tmpdir 470 mdata = open(os.path.join(cidir, "meta-data"), "w") 471 name = self.name.replace(".","-") 472 mdata.writelines(["instance-id: {}-vm-0\n".format(name), 473 "local-hostname: {}-guest\n".format(name)]) 474 mdata.close() 475 udata = open(os.path.join(cidir, "user-data"), "w") 476 print("guest user:pw {}:{}".format(self._config['guest_user'], 477 self._config['guest_pass'])) 478 udata.writelines(["#cloud-config\n", 479 "chpasswd:\n", 480 " list: |\n", 481 " root:%s\n" % self._config['root_pass'], 482 " %s:%s\n" % (self._config['guest_user'], 483 self._config['guest_pass']), 484 " expire: False\n", 485 "users:\n", 486 " - name: %s\n" % self._config['guest_user'], 487 " sudo: ALL=(ALL) NOPASSWD:ALL\n", 488 " ssh-authorized-keys:\n", 489 " - %s\n" % self._config['ssh_pub_key'], 490 " - name: root\n", 491 " ssh-authorized-keys:\n", 492 " - %s\n" % self._config['ssh_pub_key'], 493 "locale: en_US.UTF-8\n"]) 494 proxy = os.environ.get("http_proxy") 495 if not proxy is None: 496 udata.writelines(["apt:\n", 497 " proxy: %s" % proxy]) 498 udata.close() 499 subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 500 "-volid", "cidata", "-joliet", "-rock", 501 "user-data", "meta-data"], 502 cwd=cidir, 503 stdin=self._devnull, stdout=self._stdout, 504 stderr=self._stdout) 505 return os.path.join(cidir, "cloud-init.iso") 506 507 def get_qemu_packages_from_lcitool_json(self, json_path=None): 508 """Parse a lcitool variables json file and return the PKGS list.""" 509 if json_path is None: 510 json_path = os.path.join( 511 os.path.dirname(__file__), "generated", self.name + ".json" 512 ) 513 with open(json_path, "r") as fh: 514 return json.load(fh)["pkgs"] 515 516 517def get_qemu_path(arch, build_path=None): 518 """Fetch the path to the qemu binary.""" 519 # If QEMU environment variable set, it takes precedence 520 if "QEMU" in os.environ: 521 qemu_path = os.environ["QEMU"] 522 elif build_path: 523 qemu_path = os.path.join(build_path, arch + "-softmmu") 524 qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 525 else: 526 # Default is to use system path for qemu. 527 qemu_path = "qemu-system-" + arch 528 return qemu_path 529 530def get_qemu_version(qemu_path): 531 """Get the version number from the current QEMU, 532 and return the major number.""" 533 output = subprocess.check_output([qemu_path, '--version']) 534 version_line = output.decode("utf-8") 535 version_num = re.split(r' |\(', version_line)[3].split('.')[0] 536 return int(version_num) 537 538def parse_config(config, args): 539 """ Parse yaml config and populate our config structure. 540 The yaml config allows the user to override the 541 defaults for VM parameters. In many cases these 542 defaults can be overridden without rebuilding the VM.""" 543 if args.config: 544 config_file = args.config 545 elif 'QEMU_CONFIG' in os.environ: 546 config_file = os.environ['QEMU_CONFIG'] 547 else: 548 return config 549 if not os.path.exists(config_file): 550 raise Exception("config file {} does not exist".format(config_file)) 551 # We gracefully handle importing the yaml module 552 # since it might not be installed. 553 # If we are here it means the user supplied a .yml file, 554 # so if the yaml module is not installed we will exit with error. 555 try: 556 import yaml 557 except ImportError: 558 print("The python3-yaml package is needed "\ 559 "to support config.yaml files") 560 # Instead of raising an exception we exit to avoid 561 # a raft of messy (expected) errors to stdout. 562 exit(1) 563 with open(config_file) as f: 564 yaml_dict = yaml.safe_load(f) 565 566 if 'qemu-conf' in yaml_dict: 567 config.update(yaml_dict['qemu-conf']) 568 else: 569 raise Exception("config file {} is not valid"\ 570 " missing qemu-conf".format(config_file)) 571 return config 572 573def parse_args(vmcls): 574 575 def get_default_jobs(): 576 if multiprocessing.cpu_count() > 1: 577 if kvm_available(vmcls.arch): 578 return multiprocessing.cpu_count() // 2 579 elif os.uname().machine == "x86_64" and \ 580 vmcls.arch in ["aarch64", "x86_64", "i386"]: 581 # MTTCG is available on these arches and we can allow 582 # more cores. but only up to a reasonable limit. User 583 # can always override these limits with --jobs. 584 return min(multiprocessing.cpu_count() // 2, 8) 585 return 1 586 587 parser = argparse.ArgumentParser( 588 formatter_class=argparse.ArgumentDefaultsHelpFormatter, 589 description="Utility for provisioning VMs and running builds", 590 epilog="""Remaining arguments are passed to the command. 591 Exit codes: 0 = success, 1 = command line error, 592 2 = environment initialization failed, 593 3 = test command failed""") 594 parser.add_argument("--debug", "-D", action="store_true", 595 help="enable debug output") 596 parser.add_argument("--image", "-i", default="%s.img" % vmcls.name, 597 help="image file name") 598 parser.add_argument("--force", "-f", action="store_true", 599 help="force build image even if image exists") 600 parser.add_argument("--jobs", type=int, default=get_default_jobs(), 601 help="number of virtual CPUs") 602 parser.add_argument("--verbose", "-V", action="store_true", 603 help="Pass V=1 to builds within the guest") 604 parser.add_argument("--build-image", "-b", action="store_true", 605 help="build image") 606 parser.add_argument("--build-qemu", 607 help="build QEMU from source in guest") 608 parser.add_argument("--build-target", 609 help="QEMU build target", default="check") 610 parser.add_argument("--build-path", default=None, 611 help="Path of build directory, "\ 612 "for using build tree QEMU binary. ") 613 parser.add_argument("--source-path", default=None, 614 help="Path of source directory, "\ 615 "for finding additional files. ") 616 parser.add_argument("--interactive", "-I", action="store_true", 617 help="Interactively run command") 618 parser.add_argument("--snapshot", "-s", action="store_true", 619 help="run tests with a snapshot") 620 parser.add_argument("--genisoimage", default="genisoimage", 621 help="iso imaging tool") 622 parser.add_argument("--config", "-c", default=None, 623 help="Provide config yaml for configuration. "\ 624 "See config_example.yaml for example.") 625 parser.add_argument("--efi-aarch64", 626 default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 627 help="Path to efi image for aarch64 VMs.") 628 parser.add_argument("--log-console", action="store_true", 629 help="Log console to file.") 630 parser.add_argument("commands", nargs="*", help="""Remaining 631 commands after -- are passed to command inside the VM""") 632 633 return parser.parse_args() 634 635def main(vmcls, config=None): 636 try: 637 if config == None: 638 config = DEFAULT_CONFIG 639 args = parse_args(vmcls) 640 if not args.commands and not args.build_qemu and not args.build_image: 641 print("Nothing to do?") 642 return 1 643 config = parse_config(config, args) 644 logging.basicConfig(level=(logging.DEBUG if args.debug 645 else logging.WARN)) 646 vm = vmcls(args, config=config) 647 if args.build_image: 648 if os.path.exists(args.image) and not args.force: 649 sys.stderr.writelines(["Image file exists, skipping build: %s\n" % args.image, 650 "Use --force option to overwrite\n"]) 651 return 0 652 return vm.build_image(args.image) 653 if args.build_qemu: 654 vm.add_source_dir(args.build_qemu) 655 cmd = [vm.BUILD_SCRIPT.format( 656 configure_opts = " ".join(args.commands), 657 jobs=int(args.jobs), 658 target=args.build_target, 659 verbose = "V=1" if args.verbose else "")] 660 else: 661 cmd = args.commands 662 img = args.image 663 if args.snapshot: 664 img += ",snapshot=on" 665 vm.boot(img) 666 vm.wait_ssh() 667 except Exception as e: 668 if isinstance(e, SystemExit) and e.code == 0: 669 return 0 670 sys.stderr.write("Failed to prepare guest environment\n") 671 traceback.print_exc() 672 return 2 673 674 exitcode = 0 675 if vm.ssh(*cmd) != 0: 676 exitcode = 3 677 if args.interactive: 678 vm.ssh() 679 680 if not args.snapshot: 681 vm.graceful_shutdown() 682 683 return exitcode 684