]> git.proxmox.com Git - mirror_qemu.git/blame_incremental - tests/vm/basevm.py
tests: acpi: add non zero function device with acpi-index on non-hotpluggble bus
[mirror_qemu.git] / tests / vm / basevm.py
... / ...
CommitLineData
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 "-o", "IdentitiesOnly=yes"]
238 # If not in debug mode, set ssh to quiet mode to
239 # avoid printing the results of commands.
240 if not self.debug:
241 ssh_cmd.append("-q")
242 for var in self.envvars:
243 ssh_cmd += ['-o', "SendEnv=%s" % var ]
244 assert not isinstance(cmd, str)
245 ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd)
246 logging.debug("ssh_cmd: %s", " ".join(ssh_cmd))
247 r = subprocess.call(ssh_cmd)
248 if check and r != 0:
249 raise Exception("SSH command failed: %s" % cmd)
250 return r
251
252 def ssh(self, *cmd):
253 return self._ssh_do(self._config["guest_user"], cmd, False)
254
255 def ssh_root(self, *cmd):
256 return self._ssh_do(self._config["root_user"], cmd, False)
257
258 def ssh_check(self, *cmd):
259 self._ssh_do(self._config["guest_user"], cmd, True)
260
261 def ssh_root_check(self, *cmd):
262 self._ssh_do(self._config["root_user"], cmd, True)
263
264 def build_image(self, img):
265 raise NotImplementedError
266
267 def exec_qemu_img(self, *args):
268 cmd = [os.environ.get("QEMU_IMG", "qemu-img")]
269 cmd.extend(list(args))
270 subprocess.check_call(cmd)
271
272 def add_source_dir(self, src_dir):
273 name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5]
274 tarfile = os.path.join(self._tmpdir, name + ".tar")
275 logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir)
276 subprocess.check_call(["./scripts/archive-source.sh", tarfile],
277 cwd=src_dir, stdin=self._devnull,
278 stdout=self._stdout, stderr=self._stderr)
279 self._data_args += ["-drive",
280 "file=%s,if=none,id=%s,cache=writeback,format=raw" % \
281 (tarfile, name),
282 "-device",
283 "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)]
284
285 def boot(self, img, extra_args=[]):
286 boot_dev = BOOT_DEVICE[self._config['boot_dev_type']]
287 boot_params = boot_dev.format(img)
288 args = self._args + boot_params.split(' ')
289 args += self._data_args + extra_args + self._config['extra_args']
290 logging.debug("QEMU args: %s", " ".join(args))
291 qemu_path = get_qemu_path(self.arch, self._build_path)
292
293 # Since console_log_path is only set when the user provides the
294 # log_console option, we will set drain_console=True so the
295 # console is always drained.
296 guest = QEMUMachine(binary=qemu_path, args=args,
297 console_log=self._console_log_path,
298 drain_console=True)
299 guest.set_machine(self._config['machine'])
300 guest.set_console()
301 try:
302 guest.launch()
303 except:
304 logging.error("Failed to launch QEMU, command line:")
305 logging.error(" ".join([qemu_path] + args))
306 logging.error("Log:")
307 logging.error(guest.get_log())
308 logging.error("QEMU version >= 2.10 is required")
309 raise
310 atexit.register(self.shutdown)
311 self._guest = guest
312 # Init console so we can start consuming the chars.
313 self.console_init()
314 usernet_info = guest.qmp("human-monitor-command",
315 command_line="info usernet").get("return")
316 self.ssh_port = get_info_usernet_hostfwd_port(usernet_info)
317 if not self.ssh_port:
318 raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \
319 usernet_info)
320
321 def console_init(self, timeout = None):
322 if timeout == None:
323 timeout = self.socket_timeout
324 vm = self._guest
325 vm.console_socket.settimeout(timeout)
326 self.console_raw_path = os.path.join(vm._temp_dir,
327 vm._name + "-console.raw")
328 self.console_raw_file = open(self.console_raw_path, 'wb')
329
330 def console_log(self, text):
331 for line in re.split("[\r\n]", text):
332 # filter out terminal escape sequences
333 line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line)
334 line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line)
335 # replace unprintable chars
336 line = re.sub("\x1b", "<esc>", line)
337 line = re.sub("[\x00-\x1f]", ".", line)
338 line = re.sub("[\x80-\xff]", ".", line)
339 if line == "":
340 continue
341 # log console line
342 sys.stderr.write("con recv: %s\n" % line)
343
344 def console_wait(self, expect, expectalt = None):
345 vm = self._guest
346 output = ""
347 while True:
348 try:
349 chars = vm.console_socket.recv(1)
350 if self.console_raw_file:
351 self.console_raw_file.write(chars)
352 self.console_raw_file.flush()
353 except socket.timeout:
354 sys.stderr.write("console: *** read timeout ***\n")
355 sys.stderr.write("console: waiting for: '%s'\n" % expect)
356 if not expectalt is None:
357 sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt)
358 sys.stderr.write("console: line buffer:\n")
359 sys.stderr.write("\n")
360 self.console_log(output.rstrip())
361 sys.stderr.write("\n")
362 raise
363 output += chars.decode("latin1")
364 if expect in output:
365 break
366 if not expectalt is None and expectalt in output:
367 break
368 if "\r" in output or "\n" in output:
369 lines = re.split("[\r\n]", output)
370 output = lines.pop()
371 if self.debug:
372 self.console_log("\n".join(lines))
373 if self.debug:
374 self.console_log(output)
375 if not expectalt is None and expectalt in output:
376 return False
377 return True
378
379 def console_consume(self):
380 vm = self._guest
381 output = ""
382 vm.console_socket.setblocking(0)
383 while True:
384 try:
385 chars = vm.console_socket.recv(1)
386 except:
387 break
388 output += chars.decode("latin1")
389 if "\r" in output or "\n" in output:
390 lines = re.split("[\r\n]", output)
391 output = lines.pop()
392 if self.debug:
393 self.console_log("\n".join(lines))
394 if self.debug:
395 self.console_log(output)
396 vm.console_socket.setblocking(1)
397
398 def console_send(self, command):
399 vm = self._guest
400 if self.debug:
401 logline = re.sub("\n", "<enter>", command)
402 logline = re.sub("[\x00-\x1f]", ".", logline)
403 sys.stderr.write("con send: %s\n" % logline)
404 for char in list(command):
405 vm.console_socket.send(char.encode("utf-8"))
406 time.sleep(0.01)
407
408 def console_wait_send(self, wait, command):
409 self.console_wait(wait)
410 self.console_send(command)
411
412 def console_ssh_init(self, prompt, user, pw):
413 sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \
414 % self._config['ssh_pub_key'].rstrip()
415 self.console_wait_send("login:", "%s\n" % user)
416 self.console_wait_send("Password:", "%s\n" % pw)
417 self.console_wait_send(prompt, "mkdir .ssh\n")
418 self.console_wait_send(prompt, sshkey_cmd)
419 self.console_wait_send(prompt, "chmod 755 .ssh\n")
420 self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n")
421
422 def console_sshd_config(self, prompt):
423 self.console_wait(prompt)
424 self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n")
425 for var in self.envvars:
426 self.console_wait(prompt)
427 self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var)
428
429 def print_step(self, text):
430 sys.stderr.write("### %s ...\n" % text)
431
432 def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"):
433 # Allow more time for VM to boot under TCG.
434 if not kvm_available(self.arch):
435 seconds *= self.tcg_timeout_multiplier
436 starttime = datetime.datetime.now()
437 endtime = starttime + datetime.timedelta(seconds=seconds)
438 cmd_success = False
439 while datetime.datetime.now() < endtime:
440 if wait_root and self.ssh_root(cmd) == 0:
441 cmd_success = True
442 break
443 elif self.ssh(cmd) == 0:
444 cmd_success = True
445 break
446 seconds = (endtime - datetime.datetime.now()).total_seconds()
447 logging.debug("%ds before timeout", seconds)
448 time.sleep(1)
449 if not cmd_success:
450 raise Exception("Timeout while waiting for guest ssh")
451
452 def shutdown(self):
453 self._guest.shutdown(timeout=self._shutdown_timeout)
454
455 def wait(self):
456 self._guest.wait(timeout=self._shutdown_timeout)
457
458 def graceful_shutdown(self):
459 self.ssh_root(self.poweroff)
460 self._guest.wait(timeout=self._shutdown_timeout)
461
462 def qmp(self, *args, **kwargs):
463 return self._guest.qmp(*args, **kwargs)
464
465 def gen_cloud_init_iso(self):
466 cidir = self._tmpdir
467 mdata = open(os.path.join(cidir, "meta-data"), "w")
468 name = self.name.replace(".","-")
469 mdata.writelines(["instance-id: {}-vm-0\n".format(name),
470 "local-hostname: {}-guest\n".format(name)])
471 mdata.close()
472 udata = open(os.path.join(cidir, "user-data"), "w")
473 print("guest user:pw {}:{}".format(self._config['guest_user'],
474 self._config['guest_pass']))
475 udata.writelines(["#cloud-config\n",
476 "chpasswd:\n",
477 " list: |\n",
478 " root:%s\n" % self._config['root_pass'],
479 " %s:%s\n" % (self._config['guest_user'],
480 self._config['guest_pass']),
481 " expire: False\n",
482 "users:\n",
483 " - name: %s\n" % self._config['guest_user'],
484 " sudo: ALL=(ALL) NOPASSWD:ALL\n",
485 " ssh-authorized-keys:\n",
486 " - %s\n" % self._config['ssh_pub_key'],
487 " - name: root\n",
488 " ssh-authorized-keys:\n",
489 " - %s\n" % self._config['ssh_pub_key'],
490 "locale: en_US.UTF-8\n"])
491 proxy = os.environ.get("http_proxy")
492 if not proxy is None:
493 udata.writelines(["apt:\n",
494 " proxy: %s" % proxy])
495 udata.close()
496 subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso",
497 "-volid", "cidata", "-joliet", "-rock",
498 "user-data", "meta-data"],
499 cwd=cidir,
500 stdin=self._devnull, stdout=self._stdout,
501 stderr=self._stdout)
502 return os.path.join(cidir, "cloud-init.iso")
503
504def get_qemu_path(arch, build_path=None):
505 """Fetch the path to the qemu binary."""
506 # If QEMU environment variable set, it takes precedence
507 if "QEMU" in os.environ:
508 qemu_path = os.environ["QEMU"]
509 elif build_path:
510 qemu_path = os.path.join(build_path, arch + "-softmmu")
511 qemu_path = os.path.join(qemu_path, "qemu-system-" + arch)
512 else:
513 # Default is to use system path for qemu.
514 qemu_path = "qemu-system-" + arch
515 return qemu_path
516
517def get_qemu_version(qemu_path):
518 """Get the version number from the current QEMU,
519 and return the major number."""
520 output = subprocess.check_output([qemu_path, '--version'])
521 version_line = output.decode("utf-8")
522 version_num = re.split(' |\(', version_line)[3].split('.')[0]
523 return int(version_num)
524
525def parse_config(config, args):
526 """ Parse yaml config and populate our config structure.
527 The yaml config allows the user to override the
528 defaults for VM parameters. In many cases these
529 defaults can be overridden without rebuilding the VM."""
530 if args.config:
531 config_file = args.config
532 elif 'QEMU_CONFIG' in os.environ:
533 config_file = os.environ['QEMU_CONFIG']
534 else:
535 return config
536 if not os.path.exists(config_file):
537 raise Exception("config file {} does not exist".format(config_file))
538 # We gracefully handle importing the yaml module
539 # since it might not be installed.
540 # If we are here it means the user supplied a .yml file,
541 # so if the yaml module is not installed we will exit with error.
542 try:
543 import yaml
544 except ImportError:
545 print("The python3-yaml package is needed "\
546 "to support config.yaml files")
547 # Instead of raising an exception we exit to avoid
548 # a raft of messy (expected) errors to stdout.
549 exit(1)
550 with open(config_file) as f:
551 yaml_dict = yaml.safe_load(f)
552
553 if 'qemu-conf' in yaml_dict:
554 config.update(yaml_dict['qemu-conf'])
555 else:
556 raise Exception("config file {} is not valid"\
557 " missing qemu-conf".format(config_file))
558 return config
559
560def parse_args(vmcls):
561
562 def get_default_jobs():
563 if multiprocessing.cpu_count() > 1:
564 if kvm_available(vmcls.arch):
565 return multiprocessing.cpu_count() // 2
566 elif os.uname().machine == "x86_64" and \
567 vmcls.arch in ["aarch64", "x86_64", "i386"]:
568 # MTTCG is available on these arches and we can allow
569 # more cores. but only up to a reasonable limit. User
570 # can always override these limits with --jobs.
571 return min(multiprocessing.cpu_count() // 2, 8)
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