1ff2ebff0SFam Zheng#!/usr/bin/env python 2ff2ebff0SFam Zheng# 3ff2ebff0SFam Zheng# VM testing base class 4ff2ebff0SFam Zheng# 5ff2ebff0SFam Zheng# Copyright 2017 Red Hat Inc. 6ff2ebff0SFam Zheng# 7ff2ebff0SFam Zheng# Authors: 8ff2ebff0SFam Zheng# Fam Zheng <famz@redhat.com> 9ff2ebff0SFam Zheng# 10ff2ebff0SFam Zheng# This code is licensed under the GPL version 2 or later. See 11ff2ebff0SFam Zheng# the COPYING file in the top-level directory. 12ff2ebff0SFam Zheng# 13ff2ebff0SFam Zheng 14f03868bdSEduardo Habkostfrom __future__ import print_function 15ff2ebff0SFam Zhengimport os 16ff2ebff0SFam Zhengimport sys 17ff2ebff0SFam Zhengimport logging 18ff2ebff0SFam Zhengimport time 19ff2ebff0SFam Zhengimport datetime 208f8fd9edSCleber Rosasys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 21abf0bf99SJohn Snowfrom qemu import kvm_available 22abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine 23ff2ebff0SFam Zhengimport subprocess 24ff2ebff0SFam Zhengimport hashlib 25ff2ebff0SFam Zhengimport optparse 26ff2ebff0SFam Zhengimport atexit 27ff2ebff0SFam Zhengimport tempfile 28ff2ebff0SFam Zhengimport shutil 29ff2ebff0SFam Zhengimport multiprocessing 30ff2ebff0SFam Zhengimport traceback 31ff2ebff0SFam Zheng 32ff2ebff0SFam ZhengSSH_KEY = open(os.path.join(os.path.dirname(__file__), 33ff2ebff0SFam Zheng "..", "keys", "id_rsa")).read() 34ff2ebff0SFam ZhengSSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__), 35ff2ebff0SFam Zheng "..", "keys", "id_rsa.pub")).read() 36ff2ebff0SFam Zheng 37ff2ebff0SFam Zhengclass BaseVM(object): 38ff2ebff0SFam Zheng GUEST_USER = "qemu" 39ff2ebff0SFam Zheng GUEST_PASS = "qemupass" 40ff2ebff0SFam Zheng ROOT_PASS = "qemupass" 41ff2ebff0SFam Zheng 42b08ba163SGerd Hoffmann envvars = [ 43b08ba163SGerd Hoffmann "https_proxy", 44b08ba163SGerd Hoffmann "http_proxy", 45b08ba163SGerd Hoffmann "ftp_proxy", 46b08ba163SGerd Hoffmann "no_proxy", 47b08ba163SGerd Hoffmann ] 48b08ba163SGerd Hoffmann 49ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 50ff2ebff0SFam Zheng BUILD_SCRIPT = "" 51ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 52ff2ebff0SFam Zheng name = "#base" 5331719c37SPhilippe Mathieu-Daudé # The guest architecture, to be overridden by subclasses 5431719c37SPhilippe Mathieu-Daudé arch = "#arch" 55*b3f94b2fSGerd Hoffmann # command to halt the guest, can be overridden by subclasses 56*b3f94b2fSGerd Hoffmann poweroff = "poweroff" 57ff2ebff0SFam Zheng def __init__(self, debug=False, vcpus=None): 58ff2ebff0SFam Zheng self._guest = None 59ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 60ff2ebff0SFam Zheng suffix=".tmp", 61ff2ebff0SFam Zheng dir=".")) 62ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 63ff2ebff0SFam Zheng 64ff2ebff0SFam Zheng self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa") 65ff2ebff0SFam Zheng open(self._ssh_key_file, "w").write(SSH_KEY) 66ff2ebff0SFam Zheng subprocess.check_call(["chmod", "600", self._ssh_key_file]) 67ff2ebff0SFam Zheng 68ff2ebff0SFam Zheng self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 69ff2ebff0SFam Zheng open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY) 70ff2ebff0SFam Zheng 71ff2ebff0SFam Zheng self.debug = debug 72ff2ebff0SFam Zheng self._stderr = sys.stderr 73ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 74ff2ebff0SFam Zheng if self.debug: 75ff2ebff0SFam Zheng self._stdout = sys.stdout 76ff2ebff0SFam Zheng else: 77ff2ebff0SFam Zheng self._stdout = self._devnull 78ff2ebff0SFam Zheng self._args = [ \ 79eb2712f5SPeter Maydell "-nodefaults", "-m", "4G", 80b33bd859SPeter Maydell "-cpu", "max", 81ff2ebff0SFam Zheng "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22", 82ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 83ff2ebff0SFam Zheng "-vnc", "127.0.0.1:0,to=20", 84ff2ebff0SFam Zheng "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")] 85071cf5a4SPhilippe Mathieu-Daudé if vcpus and vcpus > 1: 863ace9be6SGerd Hoffmann self._args += ["-smp", "%d" % vcpus] 8771531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 88ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 89ff2ebff0SFam Zheng else: 90ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 91ff2ebff0SFam Zheng self._data_args = [] 92ff2ebff0SFam Zheng 93ff2ebff0SFam Zheng def _download_with_cache(self, url, sha256sum=None): 94ff2ebff0SFam Zheng def check_sha256sum(fname): 95ff2ebff0SFam Zheng if not sha256sum: 96ff2ebff0SFam Zheng return True 97ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 983ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 99ff2ebff0SFam Zheng 100ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 101ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 102ff2ebff0SFam Zheng os.makedirs(cache_dir) 1033ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 1043ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 105ff2ebff0SFam Zheng if os.path.exists(fname) and check_sha256sum(fname): 106ff2ebff0SFam Zheng return fname 107ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 108ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 109ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 110ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 111ff2ebff0SFam Zheng return fname 112ff2ebff0SFam Zheng 113796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 114796471e9SGerd Hoffmann ssh_cmd = ["ssh", "-q", "-t", 115ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 116ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 117ff2ebff0SFam Zheng "-o", "ConnectTimeout=1", 118ff2ebff0SFam Zheng "-p", self.ssh_port, "-i", self._ssh_key_file] 119b08ba163SGerd Hoffmann for var in self.envvars: 120b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 121ff2ebff0SFam Zheng assert not isinstance(cmd, str) 122ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 123ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 124726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 125ff2ebff0SFam Zheng if check and r != 0: 126ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 127ff2ebff0SFam Zheng return r 128ff2ebff0SFam Zheng 129ff2ebff0SFam Zheng def ssh(self, *cmd): 130ff2ebff0SFam Zheng return self._ssh_do(self.GUEST_USER, cmd, False) 131ff2ebff0SFam Zheng 132ff2ebff0SFam Zheng def ssh_root(self, *cmd): 133ff2ebff0SFam Zheng return self._ssh_do("root", cmd, False) 134ff2ebff0SFam Zheng 135ff2ebff0SFam Zheng def ssh_check(self, *cmd): 136ff2ebff0SFam Zheng self._ssh_do(self.GUEST_USER, cmd, True) 137ff2ebff0SFam Zheng 138ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 139ff2ebff0SFam Zheng self._ssh_do("root", cmd, True) 140ff2ebff0SFam Zheng 141ff2ebff0SFam Zheng def build_image(self, img): 142ff2ebff0SFam Zheng raise NotImplementedError 143ff2ebff0SFam Zheng 144ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 1453ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 146ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 147ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 148ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 149ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 150ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 151ff2ebff0SFam Zheng self._data_args += ["-drive", 152ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 153ff2ebff0SFam Zheng (tarfile, name), 154ff2ebff0SFam Zheng "-device", 155ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 156ff2ebff0SFam Zheng 157ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 158ff2ebff0SFam Zheng args = self._args + [ 159ff2ebff0SFam Zheng "-device", "VGA", 160ff2ebff0SFam Zheng "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, 161ff2ebff0SFam Zheng "-device", "virtio-blk,drive=drive0,bootindex=0"] 162ff2ebff0SFam Zheng args += self._data_args + extra_args 163ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 16431719c37SPhilippe Mathieu-Daudé qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch) 165ff2ebff0SFam Zheng guest = QEMUMachine(binary=qemu_bin, args=args) 166ff2ebff0SFam Zheng try: 167ff2ebff0SFam Zheng guest.launch() 168ff2ebff0SFam Zheng except: 169ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 170ff2ebff0SFam Zheng logging.error(" ".join([qemu_bin] + args)) 171ff2ebff0SFam Zheng logging.error("Log:") 172ff2ebff0SFam Zheng logging.error(guest.get_log()) 173ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 174ff2ebff0SFam Zheng raise 175ff2ebff0SFam Zheng atexit.register(self.shutdown) 176ff2ebff0SFam Zheng self._guest = guest 177ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 178ff2ebff0SFam Zheng command_line="info usernet") 179ff2ebff0SFam Zheng self.ssh_port = None 180ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 181ff2ebff0SFam Zheng fields = l.split() 182ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 183ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 184ff2ebff0SFam Zheng if not self.ssh_port: 185ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 186ff2ebff0SFam Zheng usernet_info) 187ff2ebff0SFam Zheng 1886b699ae1SPeter Maydell def wait_ssh(self, seconds=300): 189ff2ebff0SFam Zheng starttime = datetime.datetime.now() 190f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 191ff2ebff0SFam Zheng guest_up = False 192f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 193ff2ebff0SFam Zheng if self.ssh("exit 0") == 0: 194ff2ebff0SFam Zheng guest_up = True 195ff2ebff0SFam Zheng break 196f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 197f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 198ff2ebff0SFam Zheng time.sleep(1) 199ff2ebff0SFam Zheng if not guest_up: 200ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 201ff2ebff0SFam Zheng 202ff2ebff0SFam Zheng def shutdown(self): 203ff2ebff0SFam Zheng self._guest.shutdown() 204ff2ebff0SFam Zheng 205ff2ebff0SFam Zheng def wait(self): 206ff2ebff0SFam Zheng self._guest.wait() 207ff2ebff0SFam Zheng 208*b3f94b2fSGerd Hoffmann def graceful_shutdown(self): 209*b3f94b2fSGerd Hoffmann self.ssh_root(self.poweroff) 210*b3f94b2fSGerd Hoffmann self._guest.wait() 211*b3f94b2fSGerd Hoffmann 212ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 213ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 214ff2ebff0SFam Zheng 21563a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 2168a6e007eSPhilippe Mathieu-Daudé 2178a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 21863a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 2193ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 2208a6e007eSPhilippe Mathieu-Daudé else: 2218a6e007eSPhilippe Mathieu-Daudé return 1 2228a6e007eSPhilippe Mathieu-Daudé 223ff2ebff0SFam Zheng parser = optparse.OptionParser( 224ff2ebff0SFam Zheng description="VM test utility. Exit codes: " 225ff2ebff0SFam Zheng "0 = success, " 226ff2ebff0SFam Zheng "1 = command line error, " 227ff2ebff0SFam Zheng "2 = environment initialization failed, " 228ff2ebff0SFam Zheng "3 = test command failed") 229ff2ebff0SFam Zheng parser.add_option("--debug", "-D", action="store_true", 230ff2ebff0SFam Zheng help="enable debug output") 23163a24c5eSPhilippe Mathieu-Daudé parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 232ff2ebff0SFam Zheng help="image file name") 233ff2ebff0SFam Zheng parser.add_option("--force", "-f", action="store_true", 234ff2ebff0SFam Zheng help="force build image even if image exists") 2358a6e007eSPhilippe Mathieu-Daudé parser.add_option("--jobs", type=int, default=get_default_jobs(), 236ff2ebff0SFam Zheng help="number of virtual CPUs") 23741e3340aSPeter Maydell parser.add_option("--verbose", "-V", action="store_true", 23841e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 239ff2ebff0SFam Zheng parser.add_option("--build-image", "-b", action="store_true", 240ff2ebff0SFam Zheng help="build image") 241ff2ebff0SFam Zheng parser.add_option("--build-qemu", 242ff2ebff0SFam Zheng help="build QEMU from source in guest") 2435c2ec9b6SAlex Bennée parser.add_option("--build-target", 2445c2ec9b6SAlex Bennée help="QEMU build target", default="check") 245ff2ebff0SFam Zheng parser.add_option("--interactive", "-I", action="store_true", 246ff2ebff0SFam Zheng help="Interactively run command") 247983c2a77SFam Zheng parser.add_option("--snapshot", "-s", action="store_true", 248983c2a77SFam Zheng help="run tests with a snapshot") 249ff2ebff0SFam Zheng parser.disable_interspersed_args() 250ff2ebff0SFam Zheng return parser.parse_args() 251ff2ebff0SFam Zheng 252ff2ebff0SFam Zhengdef main(vmcls): 253ff2ebff0SFam Zheng try: 25463a24c5eSPhilippe Mathieu-Daudé args, argv = parse_args(vmcls) 255ff2ebff0SFam Zheng if not argv and not args.build_qemu and not args.build_image: 256f03868bdSEduardo Habkost print("Nothing to do?") 257ff2ebff0SFam Zheng return 1 258fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 259fb3b4e6dSEduardo Habkost else logging.WARN)) 260ff2ebff0SFam Zheng vm = vmcls(debug=args.debug, vcpus=args.jobs) 261ff2ebff0SFam Zheng if args.build_image: 262ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 263ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 264ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 265ff2ebff0SFam Zheng return 1 266ff2ebff0SFam Zheng return vm.build_image(args.image) 267ff2ebff0SFam Zheng if args.build_qemu: 268ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 269ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 270ff2ebff0SFam Zheng configure_opts = " ".join(argv), 2713ace9be6SGerd Hoffmann jobs=int(args.jobs), 2725c2ec9b6SAlex Bennée target=args.build_target, 27341e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 274ff2ebff0SFam Zheng else: 275ff2ebff0SFam Zheng cmd = argv 276983c2a77SFam Zheng img = args.image 277983c2a77SFam Zheng if args.snapshot: 278983c2a77SFam Zheng img += ",snapshot=on" 279983c2a77SFam Zheng vm.boot(img) 280ff2ebff0SFam Zheng vm.wait_ssh() 281ff2ebff0SFam Zheng except Exception as e: 282ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 283ff2ebff0SFam Zheng return 0 284ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 285ff2ebff0SFam Zheng traceback.print_exc() 286ff2ebff0SFam Zheng return 2 287ff2ebff0SFam Zheng 288*b3f94b2fSGerd Hoffmann exitcode = 0 289ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 290*b3f94b2fSGerd Hoffmann exitcode = 3 291*b3f94b2fSGerd Hoffmann if exitcode != 0 and args.interactive: 292*b3f94b2fSGerd Hoffmann vm.ssh() 293*b3f94b2fSGerd Hoffmann 294*b3f94b2fSGerd Hoffmann if not args.snapshot: 295*b3f94b2fSGerd Hoffmann vm.graceful_shutdown() 296*b3f94b2fSGerd Hoffmann 297*b3f94b2fSGerd Hoffmann return exitcode 298