master
py 690 lines 27.7 KB
Raw
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
14 import os
15 import re
16 import sys
17 import socket
18 import logging
19 import time
20 import datetime
21 import subprocess
22 import hashlib
23 import argparse
24 import atexit
25 import tempfile
26 import shutil
27 import multiprocessing
28 import traceback
29 import shlex
30 import json
31
32 from qemu.machine import QEMUMachine
33 from qemu.utils import get_info_usernet_hostfwd_port, kvm_available
34
35 SSH_KEY_FILE = os.path.join(os.path.dirname(__file__),
36 "..", "keys", "id_rsa")
37 SSH_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.
43 DEFAULT_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 }
61 BOOT_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 }
68 class 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 = 90
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 res = guest.cmd("x-query-usernet")
316 for entry in res:
317 port = get_info_usernet_hostfwd_port(entry['info'])
318 if port is not None:
319 self.ssh_port = port
320 break
321 if not self.ssh_port:
322 raise Exception("Cannot find ssh port from"
323 " 'x-query-usernet': %s" % res)
324
325 def console_init(self, timeout = None):
326 if timeout == None:
327 timeout = self.socket_timeout
328 vm = self._guest
329 vm.console_socket.settimeout(timeout)
330 self.console_raw_path = os.path.join(vm._temp_dir,
331 vm._name + "-console.raw")
332 self.console_raw_file = open(self.console_raw_path, 'wb')
333
334 def console_log(self, text):
335 for line in re.split("[\r\n]", text):
336 # filter out terminal escape sequences
337 line = re.sub("\x1b\\[[0-9;?]*[a-zA-Z]", "", line)
338 line = re.sub("\x1b\\([0-9;?]*[a-zA-Z]", "", line)
339 # replace unprintable chars
340 line = re.sub("\x1b", "<esc>", line)
341 line = re.sub("[\x00-\x1f]", ".", line)
342 line = re.sub("[\x80-\xff]", ".", line)
343 if line == "":
344 continue
345 # log console line
346 sys.stderr.write("con recv: %s\n" % line)
347
348 def console_wait(self, expect, expectalt = None):
349 vm = self._guest
350 output = ""
351 while True:
352 try:
353 chars = vm.console_socket.recv(1)
354 if self.console_raw_file:
355 self.console_raw_file.write(chars)
356 self.console_raw_file.flush()
357 except socket.timeout:
358 sys.stderr.write("console: *** read timeout ***\n")
359 sys.stderr.write("console: waiting for: '%s'\n" % expect)
360 if not expectalt is None:
361 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
362 sys.stderr.write("console: line buffer:\n")
363 sys.stderr.write("\n")
364 self.console_log(output.rstrip())
365 sys.stderr.write("\n")
366 raise
367 output += chars.decode("latin1")
368 if expect in output:
369 break
370 if not expectalt is None and expectalt in output:
371 break
372 if "\r" in output or "\n" in output:
373 lines = re.split("[\r\n]", output)
374 output = lines.pop()
375 if self.debug:
376 self.console_log("\n".join(lines))
377 if self.debug:
378 self.console_log(output)
379 if not expectalt is None and expectalt in output:
380 return False
381 return True
382
383 def console_consume(self):
384 vm = self._guest
385 output = ""
386 vm.console_socket.setblocking(0)
387 while True:
388 try:
389 chars = vm.console_socket.recv(1)
390 except:
391 break
392 output += chars.decode("latin1")
393 if "\r" in output or "\n" in output:
394 lines = re.split("[\r\n]", output)
395 output = lines.pop()
396 if self.debug:
397 self.console_log("\n".join(lines))
398 if self.debug:
399 self.console_log(output)
400 vm.console_socket.setblocking(1)
401
402 def console_send(self, command):
403 vm = self._guest
404 if self.debug:
405 logline = re.sub("\n", "<enter>", command)
406 logline = re.sub("[\x00-\x1f]", ".", logline)
407 sys.stderr.write("con send: %s\n" % logline)
408 for char in list(command):
409 vm.console_socket.send(char.encode("utf-8"))
410 time.sleep(0.01)
411
412 def console_wait_send(self, wait, command):
413 self.console_wait(wait)
414 self.console_send(command)
415
416 def console_ssh_init(self, prompt, user, pw):
417 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
418 % self._config['ssh_pub_key'].rstrip()
419 self.console_wait_send("login:", "%s\n" % user)
420 self.console_wait_send("Password:", "%s\n" % pw)
421 self.console_wait_send(prompt, "mkdir .ssh\n")
422 self.console_wait_send(prompt, sshkey_cmd)
423 self.console_wait_send(prompt, "chmod 755 .ssh\n")
424 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n")
425
426 def console_sshd_config(self, prompt):
427 self.console_wait(prompt)
428 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
429 self.console_wait(prompt)
430 self.console_send("echo 'UseDNS no' >> /etc/ssh/sshd_config\n")
431 for var in self.envvars:
432 self.console_wait(prompt)
433 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
434
435 def print_step(self, text):
436 sys.stderr.write("### %s ...\n" % text)
437
438 def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
439 # Allow more time for VM to boot under TCG.
440 if not kvm_available(self.arch):
441 seconds *= self.tcg_timeout_multiplier
442 starttime = datetime.datetime.now()
443 endtime = starttime + datetime.timedelta(seconds=seconds)
444 cmd_success = False
445 while datetime.datetime.now() < endtime:
446 if wait_root and self.ssh_root(cmd) == 0:
447 cmd_success = True
448 break
449 elif self.ssh(cmd) == 0:
450 cmd_success = True
451 break
452 seconds = (endtime - datetime.datetime.now()).total_seconds()
453 logging.debug("%ds before timeout", seconds)
454 time.sleep(1)
455 if not cmd_success:
456 raise Exception("Timeout while waiting for guest ssh")
457
458 def shutdown(self):
459 self._guest.shutdown(timeout=self._shutdown_timeout)
460
461 def wait(self):
462 self._guest.wait(timeout=self._shutdown_timeout)
463
464 def graceful_shutdown(self):
465 self.ssh_root(self.poweroff)
466 self._guest.wait(timeout=self._shutdown_timeout)
467
468 def qmp(self, *args, **kwargs):
469 return self._guest.qmp(*args, **kwargs)
470
471 def gen_cloud_init_iso(self):
472 cidir = self._tmpdir
473 mdata = open(os.path.join(cidir, "meta-data"), "w")
474 name = self.name.replace(".","-")
475 mdata.writelines(["instance-id: {}-vm-0\n".format(name),
476 "local-hostname: {}-guest\n".format(name)])
477 mdata.close()
478 udata = open(os.path.join(cidir, "user-data"), "w")
479 print("guest user:pw {}:{}".format(self._config['guest_user'],
480 self._config['guest_pass']))
481 udata.writelines(["#cloud-config\n",
482 "chpasswd:\n",
483 " list: |\n",
484 " root:%s\n" % self._config['root_pass'],
485 " %s:%s\n" % (self._config['guest_user'],
486 self._config['guest_pass']),
487 " expire: False\n",
488 "users:\n",
489 " - name: %s\n" % self._config['guest_user'],
490 " sudo: ALL=(ALL) NOPASSWD:ALL\n",
491 " ssh-authorized-keys:\n",
492 " - %s\n" % self._config['ssh_pub_key'],
493 " - name: root\n",
494 " ssh-authorized-keys:\n",
495 " - %s\n" % self._config['ssh_pub_key'],
496 "locale: en_US.UTF-8\n"])
497 proxy = os.environ.get("http_proxy")
498 if not proxy is None:
499 udata.writelines(["apt:\n",
500 " proxy: %s" % proxy])
501 udata.close()
502 subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
503 "-volid", "cidata", "-joliet", "-rock",
504 "user-data", "meta-data"],
505 cwd=cidir,
506 stdin=self._devnull, stdout=self._stdout,
507 stderr=self._stdout)
508 return os.path.join(cidir, "cloud-init.iso")
509
510 def get_qemu_packages_from_lcitool_json(self, json_path=None):
511 """Parse a lcitool variables json file and return the PKGS list."""
512 if json_path is None:
513 json_path = os.path.join(
514 os.path.dirname(__file__), "generated", self.name + ".json"
515 )
516 with open(json_path, "r") as fh:
517 return json.load(fh)["pkgs"]
518
519
520 def get_qemu_path(arch, build_path=None):
521 """Fetch the path to the qemu binary."""
522 # If QEMU environment variable set, it takes precedence
523 if "QEMU" in os.environ:
524 qemu_path = os.environ["QEMU"]
525 elif build_path:
526 qemu_path = os.path.join(build_path, "qemu-system-" + arch)
527 else:
528 # Default is to use system path for qemu.
529 qemu_path = "qemu-system-" + arch
530 return qemu_path
531
532 def get_qemu_version(qemu_path):
533 """Get the version number from the current QEMU,
534 and return the major number."""
535 output = subprocess.check_output([qemu_path, '--version'])
536 version_line = output.decode("utf-8")
537 version_num = re.split(r' |\(', version_line)[3].split('.')[0]
538 return int(version_num)
539
540 def parse_config(config, args):
541 """ Parse yaml config and populate our config structure.
542 The yaml config allows the user to override the
543 defaults for VM parameters. In many cases these
544 defaults can be overridden without rebuilding the VM."""
545 if args.config:
546 config_file = args.config
547 elif 'QEMU_CONFIG' in os.environ:
548 config_file = os.environ['QEMU_CONFIG']
549 else:
550 return config
551 if not os.path.exists(config_file):
552 raise Exception("config file {} does not exist".format(config_file))
553 # We gracefully handle importing the yaml module
554 # since it might not be installed.
555 # If we are here it means the user supplied a .yml file,
556 # so if the yaml module is not installed we will exit with error.
557 try:
558 import yaml
559 except ImportError:
560 print("The python3-yaml package is needed "\
561 "to support config.yaml files")
562 # Instead of raising an exception we exit to avoid
563 # a raft of messy (expected) errors to stdout.
564 exit(1)
565 with open(config_file) as f:
566 yaml_dict = yaml.safe_load(f)
567
568 if 'qemu-conf' in yaml_dict:
569 config.update(yaml_dict['qemu-conf'])
570 else:
571 raise Exception("config file {} is not valid"\
572 " missing qemu-conf".format(config_file))
573 return config
574
575 def parse_args(vmcls):
576
577 def get_default_jobs():
578 if multiprocessing.cpu_count() > 1:
579 if kvm_available(vmcls.arch):
580 return multiprocessing.cpu_count() // 2
581 elif os.uname().machine == "x86_64" and \
582 vmcls.arch in ["aarch64", "x86_64", "i386"]:
583 # MTTCG is available on these arches and we can allow
584 # more cores. but only up to a reasonable limit. User
585 # can always override these limits with --jobs.
586 return min(multiprocessing.cpu_count() // 2, 8)
587 return 1
588
589 parser = argparse.ArgumentParser(
590 formatter_class=argparse.ArgumentDefaultsHelpFormatter,
591 description="Utility for provisioning VMs and running builds",
592 epilog="""Remaining arguments are passed to the command.
593 Exit codes: 0 = success, 1 = command line error,
594 2 = environment initialization failed,
595 3 = test command failed""")
596 parser.add_argument("--debug", "-D", action="store_true",
597 help="enable debug output")
598 parser.add_argument("--image", "-i", default="%s.img" % vmcls.name,
599 help="image file name")
600 parser.add_argument("--force", "-f", action="store_true",
601 help="force build image even if image exists")
602 parser.add_argument("--jobs", type=int, default=get_default_jobs(),
603 help="number of virtual CPUs")
604 parser.add_argument("--verbose", "-V", action="store_true",
605 help="Pass V=1 to builds within the guest")
606 parser.add_argument("--build-image", "-b", action="store_true",
607 help="build image")
608 parser.add_argument("--build-qemu",
609 help="build QEMU from source in guest")
610 parser.add_argument("--build-target",
611 help="QEMU build target", default="all check")
612 parser.add_argument("--build-path", default=None,
613 help="Path of build directory, "\
614 "for using build tree QEMU binary. ")
615 parser.add_argument("--source-path", default=None,
616 help="Path of source directory, "\
617 "for finding additional files. ")
618 int_ops = parser.add_mutually_exclusive_group()
619 int_ops.add_argument("--interactive", "-I", action="store_true",
620 help="Interactively run command")
621 int_ops.add_argument("--interactive-root", action="store_true",
622 help="Interactively run command as root")
623 parser.add_argument("--snapshot", "-s", action="store_true",
624 help="run tests with a snapshot")
625 parser.add_argument("--genisoimage", default="genisoimage",
626 help="iso imaging tool")
627 parser.add_argument("--config", "-c", default=None,
628 help="Provide config yaml for configuration. "\
629 "See config_example.yaml for example.")
630 parser.add_argument("--efi-aarch64",
631 default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd",
632 help="Path to efi image for aarch64 VMs.")
633 parser.add_argument("--log-console", action="store_true",
634 help="Log console to file.")
635 parser.add_argument("commands", nargs="*", help="""Remaining
636 commands after -- are passed to command inside the VM""")
637
638 return parser.parse_args()
639
640 def main(vmcls, config=None):
641 try:
642 if config == None:
643 config = DEFAULT_CONFIG
644 args = parse_args(vmcls)
645 if not args.commands and not args.build_qemu and not args.build_image:
646 print("Nothing to do?")
647 return 1
648 config = parse_config(config, args)
649 logging.basicConfig(level=(logging.DEBUG if args.debug
650 else logging.WARN))
651 vm = vmcls(args, config=config)
652 if args.build_image:
653 if os.path.exists(args.image) and not args.force:
654 sys.stderr.writelines(["Image file exists, skipping build: %s\n" % args.image,
655 "Use --force option to overwrite\n"])
656 return 0
657 return vm.build_image(args.image)
658 if args.build_qemu:
659 vm.add_source_dir(args.build_qemu)
660 cmd = [vm.BUILD_SCRIPT.format(
661 configure_opts = " ".join(args.commands),
662 jobs=int(args.jobs),
663 target=args.build_target,
664 verbose = "V=1" if args.verbose else "")]
665 else:
666 cmd = args.commands
667 img = args.image
668 if args.snapshot:
669 img += ",snapshot=on"
670 vm.boot(img)
671 vm.wait_ssh()
672 except Exception as e:
673 if isinstance(e, SystemExit) and e.code == 0:
674 return 0
675 sys.stderr.write("Failed to prepare guest environment\n")
676 traceback.print_exc()
677 return 2
678
679 exitcode = 0
680 if vm.ssh(*cmd) != 0:
681 exitcode = 3
682 if args.interactive:
683 vm.ssh()
684 elif args.interactive_root:
685 vm.ssh_root()
686
687 if not args.snapshot:
688 vm.graceful_shutdown()
689
690 return exitcode