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" 55ff2ebff0SFam Zheng def __init__(self, debug=False, vcpus=None): 56ff2ebff0SFam Zheng self._guest = None 57ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 58ff2ebff0SFam Zheng suffix=".tmp", 59ff2ebff0SFam Zheng dir=".")) 60ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 61ff2ebff0SFam Zheng 62ff2ebff0SFam Zheng self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa") 63ff2ebff0SFam Zheng open(self._ssh_key_file, "w").write(SSH_KEY) 64ff2ebff0SFam Zheng subprocess.check_call(["chmod", "600", self._ssh_key_file]) 65ff2ebff0SFam Zheng 66ff2ebff0SFam Zheng self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 67ff2ebff0SFam Zheng open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY) 68ff2ebff0SFam Zheng 69ff2ebff0SFam Zheng self.debug = debug 70ff2ebff0SFam Zheng self._stderr = sys.stderr 71ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 72ff2ebff0SFam Zheng if self.debug: 73ff2ebff0SFam Zheng self._stdout = sys.stdout 74ff2ebff0SFam Zheng else: 75ff2ebff0SFam Zheng self._stdout = self._devnull 76ff2ebff0SFam Zheng self._args = [ \ 77eb2712f5SPeter Maydell "-nodefaults", "-m", "4G", 78b33bd859SPeter Maydell "-cpu", "max", 79ff2ebff0SFam Zheng "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22", 80ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 81ff2ebff0SFam Zheng "-vnc", "127.0.0.1:0,to=20", 82ff2ebff0SFam Zheng "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")] 83071cf5a4SPhilippe Mathieu-Daudé if vcpus and vcpus > 1: 843ace9be6SGerd Hoffmann self._args += ["-smp", "%d" % vcpus] 8571531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 86ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 87ff2ebff0SFam Zheng else: 88ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 89ff2ebff0SFam Zheng self._data_args = [] 90ff2ebff0SFam Zheng 91ff2ebff0SFam Zheng def _download_with_cache(self, url, sha256sum=None): 92ff2ebff0SFam Zheng def check_sha256sum(fname): 93ff2ebff0SFam Zheng if not sha256sum: 94ff2ebff0SFam Zheng return True 95ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 963ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 97ff2ebff0SFam Zheng 98ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 99ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 100ff2ebff0SFam Zheng os.makedirs(cache_dir) 1013ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 1023ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 103ff2ebff0SFam Zheng if os.path.exists(fname) and check_sha256sum(fname): 104ff2ebff0SFam Zheng return fname 105ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 106ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 107ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 108ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 109ff2ebff0SFam Zheng return fname 110ff2ebff0SFam Zheng 111*796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 112*796471e9SGerd Hoffmann ssh_cmd = ["ssh", "-q", "-t", 113ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 114ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 115ff2ebff0SFam Zheng "-o", "ConnectTimeout=1", 116ff2ebff0SFam Zheng "-p", self.ssh_port, "-i", self._ssh_key_file] 117b08ba163SGerd Hoffmann for var in self.envvars: 118b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 119ff2ebff0SFam Zheng assert not isinstance(cmd, str) 120ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 121ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 122726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 123ff2ebff0SFam Zheng if check and r != 0: 124ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 125ff2ebff0SFam Zheng return r 126ff2ebff0SFam Zheng 127ff2ebff0SFam Zheng def ssh(self, *cmd): 128ff2ebff0SFam Zheng return self._ssh_do(self.GUEST_USER, cmd, False) 129ff2ebff0SFam Zheng 130ff2ebff0SFam Zheng def ssh_root(self, *cmd): 131ff2ebff0SFam Zheng return self._ssh_do("root", cmd, False) 132ff2ebff0SFam Zheng 133ff2ebff0SFam Zheng def ssh_check(self, *cmd): 134ff2ebff0SFam Zheng self._ssh_do(self.GUEST_USER, cmd, True) 135ff2ebff0SFam Zheng 136ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 137ff2ebff0SFam Zheng self._ssh_do("root", cmd, True) 138ff2ebff0SFam Zheng 139ff2ebff0SFam Zheng def build_image(self, img): 140ff2ebff0SFam Zheng raise NotImplementedError 141ff2ebff0SFam Zheng 142ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 1433ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 144ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 145ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 146ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 147ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 148ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 149ff2ebff0SFam Zheng self._data_args += ["-drive", 150ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 151ff2ebff0SFam Zheng (tarfile, name), 152ff2ebff0SFam Zheng "-device", 153ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 154ff2ebff0SFam Zheng 155ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 156ff2ebff0SFam Zheng args = self._args + [ 157ff2ebff0SFam Zheng "-device", "VGA", 158ff2ebff0SFam Zheng "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, 159ff2ebff0SFam Zheng "-device", "virtio-blk,drive=drive0,bootindex=0"] 160ff2ebff0SFam Zheng args += self._data_args + extra_args 161ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 16231719c37SPhilippe Mathieu-Daudé qemu_bin = os.environ.get("QEMU", "qemu-system-" + self.arch) 163ff2ebff0SFam Zheng guest = QEMUMachine(binary=qemu_bin, args=args) 164ff2ebff0SFam Zheng try: 165ff2ebff0SFam Zheng guest.launch() 166ff2ebff0SFam Zheng except: 167ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 168ff2ebff0SFam Zheng logging.error(" ".join([qemu_bin] + args)) 169ff2ebff0SFam Zheng logging.error("Log:") 170ff2ebff0SFam Zheng logging.error(guest.get_log()) 171ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 172ff2ebff0SFam Zheng raise 173ff2ebff0SFam Zheng atexit.register(self.shutdown) 174ff2ebff0SFam Zheng self._guest = guest 175ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 176ff2ebff0SFam Zheng command_line="info usernet") 177ff2ebff0SFam Zheng self.ssh_port = None 178ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 179ff2ebff0SFam Zheng fields = l.split() 180ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 181ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 182ff2ebff0SFam Zheng if not self.ssh_port: 183ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 184ff2ebff0SFam Zheng usernet_info) 185ff2ebff0SFam Zheng 1866b699ae1SPeter Maydell def wait_ssh(self, seconds=300): 187ff2ebff0SFam Zheng starttime = datetime.datetime.now() 188f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 189ff2ebff0SFam Zheng guest_up = False 190f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 191ff2ebff0SFam Zheng if self.ssh("exit 0") == 0: 192ff2ebff0SFam Zheng guest_up = True 193ff2ebff0SFam Zheng break 194f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 195f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 196ff2ebff0SFam Zheng time.sleep(1) 197ff2ebff0SFam Zheng if not guest_up: 198ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 199ff2ebff0SFam Zheng 200ff2ebff0SFam Zheng def shutdown(self): 201ff2ebff0SFam Zheng self._guest.shutdown() 202ff2ebff0SFam Zheng 203ff2ebff0SFam Zheng def wait(self): 204ff2ebff0SFam Zheng self._guest.wait() 205ff2ebff0SFam Zheng 206ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 207ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 208ff2ebff0SFam Zheng 20963a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 2108a6e007eSPhilippe Mathieu-Daudé 2118a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 21263a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 2133ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 2148a6e007eSPhilippe Mathieu-Daudé else: 2158a6e007eSPhilippe Mathieu-Daudé return 1 2168a6e007eSPhilippe Mathieu-Daudé 217ff2ebff0SFam Zheng parser = optparse.OptionParser( 218ff2ebff0SFam Zheng description="VM test utility. Exit codes: " 219ff2ebff0SFam Zheng "0 = success, " 220ff2ebff0SFam Zheng "1 = command line error, " 221ff2ebff0SFam Zheng "2 = environment initialization failed, " 222ff2ebff0SFam Zheng "3 = test command failed") 223ff2ebff0SFam Zheng parser.add_option("--debug", "-D", action="store_true", 224ff2ebff0SFam Zheng help="enable debug output") 22563a24c5eSPhilippe Mathieu-Daudé parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 226ff2ebff0SFam Zheng help="image file name") 227ff2ebff0SFam Zheng parser.add_option("--force", "-f", action="store_true", 228ff2ebff0SFam Zheng help="force build image even if image exists") 2298a6e007eSPhilippe Mathieu-Daudé parser.add_option("--jobs", type=int, default=get_default_jobs(), 230ff2ebff0SFam Zheng help="number of virtual CPUs") 23141e3340aSPeter Maydell parser.add_option("--verbose", "-V", action="store_true", 23241e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 233ff2ebff0SFam Zheng parser.add_option("--build-image", "-b", action="store_true", 234ff2ebff0SFam Zheng help="build image") 235ff2ebff0SFam Zheng parser.add_option("--build-qemu", 236ff2ebff0SFam Zheng help="build QEMU from source in guest") 2375c2ec9b6SAlex Bennée parser.add_option("--build-target", 2385c2ec9b6SAlex Bennée help="QEMU build target", default="check") 239ff2ebff0SFam Zheng parser.add_option("--interactive", "-I", action="store_true", 240ff2ebff0SFam Zheng help="Interactively run command") 241983c2a77SFam Zheng parser.add_option("--snapshot", "-s", action="store_true", 242983c2a77SFam Zheng help="run tests with a snapshot") 243ff2ebff0SFam Zheng parser.disable_interspersed_args() 244ff2ebff0SFam Zheng return parser.parse_args() 245ff2ebff0SFam Zheng 246ff2ebff0SFam Zhengdef main(vmcls): 247ff2ebff0SFam Zheng try: 24863a24c5eSPhilippe Mathieu-Daudé args, argv = parse_args(vmcls) 249ff2ebff0SFam Zheng if not argv and not args.build_qemu and not args.build_image: 250f03868bdSEduardo Habkost print("Nothing to do?") 251ff2ebff0SFam Zheng return 1 252fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 253fb3b4e6dSEduardo Habkost else logging.WARN)) 254ff2ebff0SFam Zheng vm = vmcls(debug=args.debug, vcpus=args.jobs) 255ff2ebff0SFam Zheng if args.build_image: 256ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 257ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 258ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 259ff2ebff0SFam Zheng return 1 260ff2ebff0SFam Zheng return vm.build_image(args.image) 261ff2ebff0SFam Zheng if args.build_qemu: 262ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 263ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 264ff2ebff0SFam Zheng configure_opts = " ".join(argv), 2653ace9be6SGerd Hoffmann jobs=int(args.jobs), 2665c2ec9b6SAlex Bennée target=args.build_target, 26741e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 268ff2ebff0SFam Zheng else: 269ff2ebff0SFam Zheng cmd = argv 270983c2a77SFam Zheng img = args.image 271983c2a77SFam Zheng if args.snapshot: 272983c2a77SFam Zheng img += ",snapshot=on" 273983c2a77SFam Zheng vm.boot(img) 274ff2ebff0SFam Zheng vm.wait_ssh() 275ff2ebff0SFam Zheng except Exception as e: 276ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 277ff2ebff0SFam Zheng return 0 278ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 279ff2ebff0SFam Zheng traceback.print_exc() 280ff2ebff0SFam Zheng return 2 281ff2ebff0SFam Zheng 282ff2ebff0SFam Zheng if args.interactive: 283*796471e9SGerd Hoffmann if vm.ssh(*cmd) == 0: 284ff2ebff0SFam Zheng return 0 285*796471e9SGerd Hoffmann vm.ssh() 286ff2ebff0SFam Zheng return 3 287ff2ebff0SFam Zheng else: 288ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 289ff2ebff0SFam Zheng return 3 290