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 20ff2ebff0SFam Zhengsys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "scripts")) 21ff2ebff0SFam Zhengfrom qemu import QEMUMachine 22ff2ebff0SFam Zhengimport subprocess 23ff2ebff0SFam Zhengimport hashlib 24ff2ebff0SFam Zhengimport optparse 25ff2ebff0SFam Zhengimport atexit 26ff2ebff0SFam Zhengimport tempfile 27ff2ebff0SFam Zhengimport shutil 28ff2ebff0SFam Zhengimport multiprocessing 29ff2ebff0SFam Zhengimport traceback 30ff2ebff0SFam Zheng 31ff2ebff0SFam ZhengSSH_KEY = open(os.path.join(os.path.dirname(__file__), 32ff2ebff0SFam Zheng "..", "keys", "id_rsa")).read() 33ff2ebff0SFam ZhengSSH_PUB_KEY = open(os.path.join(os.path.dirname(__file__), 34ff2ebff0SFam Zheng "..", "keys", "id_rsa.pub")).read() 35ff2ebff0SFam Zheng 36ff2ebff0SFam Zhengclass BaseVM(object): 37ff2ebff0SFam Zheng GUEST_USER = "qemu" 38ff2ebff0SFam Zheng GUEST_PASS = "qemupass" 39ff2ebff0SFam Zheng ROOT_PASS = "qemupass" 40ff2ebff0SFam Zheng 41ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 42ff2ebff0SFam Zheng BUILD_SCRIPT = "" 43ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 44ff2ebff0SFam Zheng name = "#base" 45ff2ebff0SFam Zheng def __init__(self, debug=False, vcpus=None): 46ff2ebff0SFam Zheng self._guest = None 47ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 48ff2ebff0SFam Zheng suffix=".tmp", 49ff2ebff0SFam Zheng dir=".")) 50ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 51ff2ebff0SFam Zheng 52ff2ebff0SFam Zheng self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa") 53ff2ebff0SFam Zheng open(self._ssh_key_file, "w").write(SSH_KEY) 54ff2ebff0SFam Zheng subprocess.check_call(["chmod", "600", self._ssh_key_file]) 55ff2ebff0SFam Zheng 56ff2ebff0SFam Zheng self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 57ff2ebff0SFam Zheng open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY) 58ff2ebff0SFam Zheng 59ff2ebff0SFam Zheng self.debug = debug 60ff2ebff0SFam Zheng self._stderr = sys.stderr 61ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 62ff2ebff0SFam Zheng if self.debug: 63ff2ebff0SFam Zheng self._stdout = sys.stdout 64ff2ebff0SFam Zheng else: 65ff2ebff0SFam Zheng self._stdout = self._devnull 66ff2ebff0SFam Zheng self._args = [ \ 67eb2712f5SPeter Maydell "-nodefaults", "-m", "4G", 68*b33bd859SPeter Maydell "-cpu", "max", 69ff2ebff0SFam Zheng "-netdev", "user,id=vnet,hostfwd=:127.0.0.1:0-:22", 70ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 71ff2ebff0SFam Zheng "-vnc", "127.0.0.1:0,to=20", 72ff2ebff0SFam Zheng "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")] 73ff2ebff0SFam Zheng if vcpus: 74ff2ebff0SFam Zheng self._args += ["-smp", str(vcpus)] 75ff2ebff0SFam Zheng if os.access("/dev/kvm", os.R_OK | os.W_OK): 76ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 77ff2ebff0SFam Zheng else: 78ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 79ff2ebff0SFam Zheng self._data_args = [] 80ff2ebff0SFam Zheng 81ff2ebff0SFam Zheng def _download_with_cache(self, url, sha256sum=None): 82ff2ebff0SFam Zheng def check_sha256sum(fname): 83ff2ebff0SFam Zheng if not sha256sum: 84ff2ebff0SFam Zheng return True 85ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 86ff2ebff0SFam Zheng return sha256sum == checksum 87ff2ebff0SFam Zheng 88ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 89ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 90ff2ebff0SFam Zheng os.makedirs(cache_dir) 91ff2ebff0SFam Zheng fname = os.path.join(cache_dir, hashlib.sha1(url).hexdigest()) 92ff2ebff0SFam Zheng if os.path.exists(fname) and check_sha256sum(fname): 93ff2ebff0SFam Zheng return fname 94ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 95ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 96ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 97ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 98ff2ebff0SFam Zheng return fname 99ff2ebff0SFam Zheng 100ff2ebff0SFam Zheng def _ssh_do(self, user, cmd, check, interactive=False): 101ff2ebff0SFam Zheng ssh_cmd = ["ssh", "-q", 102ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 103ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 104ff2ebff0SFam Zheng "-o", "ConnectTimeout=1", 105ff2ebff0SFam Zheng "-p", self.ssh_port, "-i", self._ssh_key_file] 106ff2ebff0SFam Zheng if interactive: 107ff2ebff0SFam Zheng ssh_cmd += ['-t'] 108ff2ebff0SFam Zheng assert not isinstance(cmd, str) 109ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 110ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 111726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 112ff2ebff0SFam Zheng if check and r != 0: 113ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 114ff2ebff0SFam Zheng return r 115ff2ebff0SFam Zheng 116ff2ebff0SFam Zheng def ssh(self, *cmd): 117ff2ebff0SFam Zheng return self._ssh_do(self.GUEST_USER, cmd, False) 118ff2ebff0SFam Zheng 119ff2ebff0SFam Zheng def ssh_interactive(self, *cmd): 120ff2ebff0SFam Zheng return self._ssh_do(self.GUEST_USER, cmd, False, True) 121ff2ebff0SFam Zheng 122ff2ebff0SFam Zheng def ssh_root(self, *cmd): 123ff2ebff0SFam Zheng return self._ssh_do("root", cmd, False) 124ff2ebff0SFam Zheng 125ff2ebff0SFam Zheng def ssh_check(self, *cmd): 126ff2ebff0SFam Zheng self._ssh_do(self.GUEST_USER, cmd, True) 127ff2ebff0SFam Zheng 128ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 129ff2ebff0SFam Zheng self._ssh_do("root", cmd, True) 130ff2ebff0SFam Zheng 131ff2ebff0SFam Zheng def build_image(self, img): 132ff2ebff0SFam Zheng raise NotImplementedError 133ff2ebff0SFam Zheng 134ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 135ff2ebff0SFam Zheng name = "data-" + hashlib.sha1(src_dir).hexdigest()[:5] 136ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 137ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 138ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 139ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 140ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 141ff2ebff0SFam Zheng self._data_args += ["-drive", 142ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 143ff2ebff0SFam Zheng (tarfile, name), 144ff2ebff0SFam Zheng "-device", 145ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 146ff2ebff0SFam Zheng 147ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 148ff2ebff0SFam Zheng args = self._args + [ 149ff2ebff0SFam Zheng "-device", "VGA", 150ff2ebff0SFam Zheng "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, 151ff2ebff0SFam Zheng "-device", "virtio-blk,drive=drive0,bootindex=0"] 152ff2ebff0SFam Zheng args += self._data_args + extra_args 153ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 154ff2ebff0SFam Zheng qemu_bin = os.environ.get("QEMU", "qemu-system-x86_64") 155ff2ebff0SFam Zheng guest = QEMUMachine(binary=qemu_bin, args=args) 156ff2ebff0SFam Zheng try: 157ff2ebff0SFam Zheng guest.launch() 158ff2ebff0SFam Zheng except: 159ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 160ff2ebff0SFam Zheng logging.error(" ".join([qemu_bin] + args)) 161ff2ebff0SFam Zheng logging.error("Log:") 162ff2ebff0SFam Zheng logging.error(guest.get_log()) 163ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 164ff2ebff0SFam Zheng raise 165ff2ebff0SFam Zheng atexit.register(self.shutdown) 166ff2ebff0SFam Zheng self._guest = guest 167ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 168ff2ebff0SFam Zheng command_line="info usernet") 169ff2ebff0SFam Zheng self.ssh_port = None 170ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 171ff2ebff0SFam Zheng fields = l.split() 172ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 173ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 174ff2ebff0SFam Zheng if not self.ssh_port: 175ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 176ff2ebff0SFam Zheng usernet_info) 177ff2ebff0SFam Zheng 1786b699ae1SPeter Maydell def wait_ssh(self, seconds=300): 179ff2ebff0SFam Zheng starttime = datetime.datetime.now() 180ff2ebff0SFam Zheng guest_up = False 181ff2ebff0SFam Zheng while (datetime.datetime.now() - starttime).total_seconds() < seconds: 182ff2ebff0SFam Zheng if self.ssh("exit 0") == 0: 183ff2ebff0SFam Zheng guest_up = True 184ff2ebff0SFam Zheng break 185ff2ebff0SFam Zheng time.sleep(1) 186ff2ebff0SFam Zheng if not guest_up: 187ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 188ff2ebff0SFam Zheng 189ff2ebff0SFam Zheng def shutdown(self): 190ff2ebff0SFam Zheng self._guest.shutdown() 191ff2ebff0SFam Zheng 192ff2ebff0SFam Zheng def wait(self): 193ff2ebff0SFam Zheng self._guest.wait() 194ff2ebff0SFam Zheng 195ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 196ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 197ff2ebff0SFam Zheng 198ff2ebff0SFam Zhengdef parse_args(vm_name): 199ff2ebff0SFam Zheng parser = optparse.OptionParser( 200ff2ebff0SFam Zheng description="VM test utility. Exit codes: " 201ff2ebff0SFam Zheng "0 = success, " 202ff2ebff0SFam Zheng "1 = command line error, " 203ff2ebff0SFam Zheng "2 = environment initialization failed, " 204ff2ebff0SFam Zheng "3 = test command failed") 205ff2ebff0SFam Zheng parser.add_option("--debug", "-D", action="store_true", 206ff2ebff0SFam Zheng help="enable debug output") 207ff2ebff0SFam Zheng parser.add_option("--image", "-i", default="%s.img" % vm_name, 208ff2ebff0SFam Zheng help="image file name") 209ff2ebff0SFam Zheng parser.add_option("--force", "-f", action="store_true", 210ff2ebff0SFam Zheng help="force build image even if image exists") 211ff2ebff0SFam Zheng parser.add_option("--jobs", type=int, default=multiprocessing.cpu_count() / 2, 212ff2ebff0SFam Zheng help="number of virtual CPUs") 21341e3340aSPeter Maydell parser.add_option("--verbose", "-V", action="store_true", 21441e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 215ff2ebff0SFam Zheng parser.add_option("--build-image", "-b", action="store_true", 216ff2ebff0SFam Zheng help="build image") 217ff2ebff0SFam Zheng parser.add_option("--build-qemu", 218ff2ebff0SFam Zheng help="build QEMU from source in guest") 219ff2ebff0SFam Zheng parser.add_option("--interactive", "-I", action="store_true", 220ff2ebff0SFam Zheng help="Interactively run command") 221983c2a77SFam Zheng parser.add_option("--snapshot", "-s", action="store_true", 222983c2a77SFam Zheng help="run tests with a snapshot") 223ff2ebff0SFam Zheng parser.disable_interspersed_args() 224ff2ebff0SFam Zheng return parser.parse_args() 225ff2ebff0SFam Zheng 226ff2ebff0SFam Zhengdef main(vmcls): 227ff2ebff0SFam Zheng try: 228ff2ebff0SFam Zheng args, argv = parse_args(vmcls.name) 229ff2ebff0SFam Zheng if not argv and not args.build_qemu and not args.build_image: 230f03868bdSEduardo Habkost print("Nothing to do?") 231ff2ebff0SFam Zheng return 1 232fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 233fb3b4e6dSEduardo Habkost else logging.WARN)) 234ff2ebff0SFam Zheng vm = vmcls(debug=args.debug, vcpus=args.jobs) 235ff2ebff0SFam Zheng if args.build_image: 236ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 237ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 238ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 239ff2ebff0SFam Zheng return 1 240ff2ebff0SFam Zheng return vm.build_image(args.image) 241ff2ebff0SFam Zheng if args.build_qemu: 242ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 243ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 244ff2ebff0SFam Zheng configure_opts = " ".join(argv), 24541e3340aSPeter Maydell jobs=args.jobs, 24641e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 247ff2ebff0SFam Zheng else: 248ff2ebff0SFam Zheng cmd = argv 249983c2a77SFam Zheng img = args.image 250983c2a77SFam Zheng if args.snapshot: 251983c2a77SFam Zheng img += ",snapshot=on" 252983c2a77SFam Zheng vm.boot(img) 253ff2ebff0SFam Zheng vm.wait_ssh() 254ff2ebff0SFam Zheng except Exception as e: 255ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 256ff2ebff0SFam Zheng return 0 257ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 258ff2ebff0SFam Zheng traceback.print_exc() 259ff2ebff0SFam Zheng return 2 260ff2ebff0SFam Zheng 261ff2ebff0SFam Zheng if args.interactive: 262ff2ebff0SFam Zheng if vm.ssh_interactive(*cmd) == 0: 263ff2ebff0SFam Zheng return 0 264ff2ebff0SFam Zheng vm.ssh_interactive() 265ff2ebff0SFam Zheng return 3 266ff2ebff0SFam Zheng else: 267ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 268ff2ebff0SFam Zheng return 3 269