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