1ff2ebff0SFam Zheng# 2ff2ebff0SFam Zheng# VM testing base class 3ff2ebff0SFam Zheng# 48dd38334SGerd Hoffmann# Copyright 2017-2019 Red Hat Inc. 5ff2ebff0SFam Zheng# 6ff2ebff0SFam Zheng# Authors: 7ff2ebff0SFam Zheng# Fam Zheng <famz@redhat.com> 88dd38334SGerd Hoffmann# Gerd Hoffmann <kraxel@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 14ff2ebff0SFam Zhengimport os 158dd38334SGerd Hoffmannimport re 16ff2ebff0SFam Zhengimport sys 178dd38334SGerd Hoffmannimport socket 18ff2ebff0SFam Zhengimport logging 19ff2ebff0SFam Zhengimport time 20ff2ebff0SFam Zhengimport datetime 218f8fd9edSCleber Rosasys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', 'python')) 228b272e00SWainer dos Santos Moschettafrom qemu.accel import kvm_available 23abf0bf99SJohn Snowfrom qemu.machine import QEMUMachine 24ff2ebff0SFam Zhengimport subprocess 25ff2ebff0SFam Zhengimport hashlib 26ff2ebff0SFam Zhengimport optparse 27ff2ebff0SFam Zhengimport atexit 28ff2ebff0SFam Zhengimport tempfile 29ff2ebff0SFam Zhengimport shutil 30ff2ebff0SFam Zhengimport multiprocessing 31ff2ebff0SFam Zhengimport traceback 325d676197SRobert Foleyimport shlex 33ff2ebff0SFam Zheng 345d676197SRobert FoleySSH_KEY_FILE = os.path.join(os.path.dirname(__file__), 355d676197SRobert Foley "..", "keys", "id_rsa") 365d676197SRobert FoleySSH_PUB_KEY_FILE = os.path.join(os.path.dirname(__file__), 375d676197SRobert Foley "..", "keys", "id_rsa.pub") 38ff2ebff0SFam Zheng 395d676197SRobert Foley# This is the standard configuration. 405d676197SRobert Foley# Any or all of these can be overridden by 415d676197SRobert Foley# passing in a config argument to the VM constructor. 425d676197SRobert FoleyDEFAULT_CONFIG = { 435d676197SRobert Foley 'cpu' : "max", 445d676197SRobert Foley 'machine' : 'pc', 455d676197SRobert Foley 'guest_user' : "qemu", 465d676197SRobert Foley 'guest_pass' : "qemupass", 475d676197SRobert Foley 'root_pass' : "qemupass", 485d676197SRobert Foley 'ssh_key_file' : SSH_KEY_FILE, 495d676197SRobert Foley 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 505d676197SRobert Foley 'memory' : "4G", 515d676197SRobert Foley 'extra_args' : [], 525d676197SRobert Foley 'qemu_args' : "", 535d676197SRobert Foley 'dns' : "", 545d676197SRobert Foley 'ssh_port' : 0, 555d676197SRobert Foley 'install_cmds' : "", 565d676197SRobert Foley 'boot_dev_type' : "block", 575d676197SRobert Foley 'ssh_timeout' : 1, 585d676197SRobert Foley} 595d676197SRobert FoleyBOOT_DEVICE = { 605d676197SRobert Foley 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 615d676197SRobert Foley "-device virtio-blk,drive=drive0,bootindex=0", 625d676197SRobert Foley 'scsi' : "-device virtio-scsi-device,id=scsi "\ 635d676197SRobert Foley "-drive file={},format=raw,if=none,id=hd0 "\ 645d676197SRobert Foley "-device scsi-hd,drive=hd0,bootindex=0", 655d676197SRobert Foley} 66ff2ebff0SFam Zhengclass BaseVM(object): 67ff2ebff0SFam Zheng 68b08ba163SGerd Hoffmann envvars = [ 69b08ba163SGerd Hoffmann "https_proxy", 70b08ba163SGerd Hoffmann "http_proxy", 71b08ba163SGerd Hoffmann "ftp_proxy", 72b08ba163SGerd Hoffmann "no_proxy", 73b08ba163SGerd Hoffmann ] 74b08ba163SGerd Hoffmann 75ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 76ff2ebff0SFam Zheng BUILD_SCRIPT = "" 77ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 78ff2ebff0SFam Zheng name = "#base" 7931719c37SPhilippe Mathieu-Daudé # The guest architecture, to be overridden by subclasses 8031719c37SPhilippe Mathieu-Daudé arch = "#arch" 81b3f94b2fSGerd Hoffmann # command to halt the guest, can be overridden by subclasses 82b3f94b2fSGerd Hoffmann poweroff = "poweroff" 835b790481SEduardo Habkost # enable IPv6 networking 845b790481SEduardo Habkost ipv6 = True 855d676197SRobert Foley # This is the timeout on the wait for console bytes. 865d676197SRobert Foley socket_timeout = 120 87c9de3935SRobert Foley # Scale up some timeouts under TCG. 88c9de3935SRobert Foley # 4 is arbitrary, but greater than 2, 89c9de3935SRobert Foley # since we found we need to wait more than twice as long. 90c9de3935SRobert Foley tcg_ssh_timeout_multiplier = 4 915d676197SRobert Foley def __init__(self, args, config=None): 92ff2ebff0SFam Zheng self._guest = None 931f335d18SRobert Foley self._genisoimage = args.genisoimage 941f335d18SRobert Foley self._build_path = args.build_path 9513336606SRobert Foley self._efi_aarch64 = args.efi_aarch64 965d676197SRobert Foley # Allow input config to override defaults. 975d676197SRobert Foley self._config = DEFAULT_CONFIG.copy() 985d676197SRobert Foley if config != None: 995d676197SRobert Foley self._config.update(config) 1005d676197SRobert Foley self.validate_ssh_keys() 101ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 102ff2ebff0SFam Zheng suffix=".tmp", 103ff2ebff0SFam Zheng dir=".")) 104ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 1055d676197SRobert Foley # Copy the key files to a temporary directory. 1065d676197SRobert Foley # Also chmod the key file to agree with ssh requirements. 1075d676197SRobert Foley self._config['ssh_key'] = \ 1085d676197SRobert Foley open(self._config['ssh_key_file']).read().rstrip() 1095d676197SRobert Foley self._config['ssh_pub_key'] = \ 1105d676197SRobert Foley open(self._config['ssh_pub_key_file']).read().rstrip() 1115d676197SRobert Foley self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa") 1125d676197SRobert Foley open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key']) 1135d676197SRobert Foley subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file]) 114ff2ebff0SFam Zheng 1155d676197SRobert Foley self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 1165d676197SRobert Foley open(self._ssh_tmp_pub_key_file, 1175d676197SRobert Foley "w").write(self._config['ssh_pub_key']) 118ff2ebff0SFam Zheng 1191f335d18SRobert Foley self.debug = args.debug 120*ff14ab0cSRobert Foley self._console_log_path = None 121*ff14ab0cSRobert Foley if args.log_console: 122*ff14ab0cSRobert Foley self._console_log_path = \ 123*ff14ab0cSRobert Foley os.path.join(os.path.expanduser("~/.cache/qemu-vm"), 124*ff14ab0cSRobert Foley "{}.install.log".format(self.name)) 125ff2ebff0SFam Zheng self._stderr = sys.stderr 126ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 127ff2ebff0SFam Zheng if self.debug: 128ff2ebff0SFam Zheng self._stdout = sys.stdout 129ff2ebff0SFam Zheng else: 130ff2ebff0SFam Zheng self._stdout = self._devnull 1315d676197SRobert Foley netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22" 132ff2ebff0SFam Zheng self._args = [ \ 1335d676197SRobert Foley "-nodefaults", "-m", self._config['memory'], 1345d676197SRobert Foley "-cpu", self._config['cpu'], 1355d676197SRobert Foley "-netdev", 1365d676197SRobert Foley netdev.format(self._config['ssh_port']) + 1375d676197SRobert Foley (",ipv6=no" if not self.ipv6 else "") + 1385d676197SRobert Foley (",dns=" + self._config['dns'] if self._config['dns'] else ""), 139ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 1408dd38334SGerd Hoffmann "-vnc", "127.0.0.1:0,to=20"] 1411f335d18SRobert Foley if args.jobs and args.jobs > 1: 1421f335d18SRobert Foley self._args += ["-smp", "%d" % args.jobs] 14371531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 144ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 145ff2ebff0SFam Zheng else: 146ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 147ff2ebff0SFam Zheng self._data_args = [] 148ff2ebff0SFam Zheng 1495d676197SRobert Foley if self._config['qemu_args'] != None: 1505d676197SRobert Foley qemu_args = self._config['qemu_args'] 1515d676197SRobert Foley qemu_args = qemu_args.replace('\n',' ').replace('\r','') 1525d676197SRobert Foley # shlex groups quoted arguments together 1535d676197SRobert Foley # we need this to keep the quoted args together for when 1545d676197SRobert Foley # the QEMU command is issued later. 1555d676197SRobert Foley args = shlex.split(qemu_args) 1565d676197SRobert Foley self._config['extra_args'] = [] 1575d676197SRobert Foley for arg in args: 1585d676197SRobert Foley if arg: 1595d676197SRobert Foley # Preserve quotes around arguments. 1605d676197SRobert Foley # shlex above takes them out, so add them in. 1615d676197SRobert Foley if " " in arg: 1625d676197SRobert Foley arg = '"{}"'.format(arg) 1635d676197SRobert Foley self._config['extra_args'].append(arg) 1645d676197SRobert Foley 1655d676197SRobert Foley def validate_ssh_keys(self): 1665d676197SRobert Foley """Check to see if the ssh key files exist.""" 1675d676197SRobert Foley if 'ssh_key_file' not in self._config or\ 1685d676197SRobert Foley not os.path.exists(self._config['ssh_key_file']): 1695d676197SRobert Foley raise Exception("ssh key file not found.") 1705d676197SRobert Foley if 'ssh_pub_key_file' not in self._config or\ 1715d676197SRobert Foley not os.path.exists(self._config['ssh_pub_key_file']): 1725d676197SRobert Foley raise Exception("ssh pub key file not found.") 1735d676197SRobert Foley 1745d676197SRobert Foley def wait_boot(self, wait_string=None): 1755d676197SRobert Foley """Wait for the standard string we expect 1765d676197SRobert Foley on completion of a normal boot. 1775d676197SRobert Foley The user can also choose to override with an 1785d676197SRobert Foley alternate string to wait for.""" 1795d676197SRobert Foley if wait_string is None: 1805d676197SRobert Foley if self.login_prompt is None: 1815d676197SRobert Foley raise Exception("self.login_prompt not defined") 1825d676197SRobert Foley wait_string = self.login_prompt 1835d676197SRobert Foley # Intentionally bump up the default timeout under TCG, 1845d676197SRobert Foley # since the console wait below takes longer. 1855d676197SRobert Foley timeout = self.socket_timeout 1865d676197SRobert Foley if not kvm_available(self.arch): 1875d676197SRobert Foley timeout *= 8 1885d676197SRobert Foley self.console_init(timeout=timeout) 1895d676197SRobert Foley self.console_wait(wait_string) 1905d676197SRobert Foley 1915b4b4865SAlex Bennée def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 192ff2ebff0SFam Zheng def check_sha256sum(fname): 193ff2ebff0SFam Zheng if not sha256sum: 194ff2ebff0SFam Zheng return True 195ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 1963ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 197ff2ebff0SFam Zheng 1985b4b4865SAlex Bennée def check_sha512sum(fname): 1995b4b4865SAlex Bennée if not sha512sum: 2005b4b4865SAlex Bennée return True 2015b4b4865SAlex Bennée checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 2025b4b4865SAlex Bennée return sha512sum == checksum.decode("utf-8") 2035b4b4865SAlex Bennée 204ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 205ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 206ff2ebff0SFam Zheng os.makedirs(cache_dir) 2073ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 2083ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 2095b4b4865SAlex Bennée if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 210ff2ebff0SFam Zheng return fname 211ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 212ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 213ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 214ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 215ff2ebff0SFam Zheng return fname 216ff2ebff0SFam Zheng 217796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 21889adc5b9SRobert Foley ssh_cmd = ["ssh", 21989adc5b9SRobert Foley "-t", 220ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 221ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 2225d676197SRobert Foley "-o", 2235d676197SRobert Foley "ConnectTimeout={}".format(self._config["ssh_timeout"]), 2245d676197SRobert Foley "-p", self.ssh_port, "-i", self._ssh_tmp_key_file] 22589adc5b9SRobert Foley # If not in debug mode, set ssh to quiet mode to 22689adc5b9SRobert Foley # avoid printing the results of commands. 22789adc5b9SRobert Foley if not self.debug: 22889adc5b9SRobert Foley ssh_cmd.append("-q") 229b08ba163SGerd Hoffmann for var in self.envvars: 230b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 231ff2ebff0SFam Zheng assert not isinstance(cmd, str) 232ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 233ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 234726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 235ff2ebff0SFam Zheng if check and r != 0: 236ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 237ff2ebff0SFam Zheng return r 238ff2ebff0SFam Zheng 239ff2ebff0SFam Zheng def ssh(self, *cmd): 240df001680SRobert Foley return self._ssh_do(self._config["guest_user"], cmd, False) 241ff2ebff0SFam Zheng 242ff2ebff0SFam Zheng def ssh_root(self, *cmd): 243ff2ebff0SFam Zheng return self._ssh_do("root", cmd, False) 244ff2ebff0SFam Zheng 245ff2ebff0SFam Zheng def ssh_check(self, *cmd): 246df001680SRobert Foley self._ssh_do(self._config["guest_user"], cmd, True) 247ff2ebff0SFam Zheng 248ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 249ff2ebff0SFam Zheng self._ssh_do("root", cmd, True) 250ff2ebff0SFam Zheng 251ff2ebff0SFam Zheng def build_image(self, img): 252ff2ebff0SFam Zheng raise NotImplementedError 253ff2ebff0SFam Zheng 2541e48931cSWainer dos Santos Moschetta def exec_qemu_img(self, *args): 2551e48931cSWainer dos Santos Moschetta cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 2561e48931cSWainer dos Santos Moschetta cmd.extend(list(args)) 2571e48931cSWainer dos Santos Moschetta subprocess.check_call(cmd) 2581e48931cSWainer dos Santos Moschetta 259ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 2603ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 261ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 262ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 263ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 264ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 265ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 266ff2ebff0SFam Zheng self._data_args += ["-drive", 267ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 268ff2ebff0SFam Zheng (tarfile, name), 269ff2ebff0SFam Zheng "-device", 270ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 271ff2ebff0SFam Zheng 272ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 2735d676197SRobert Foley boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 2745d676197SRobert Foley boot_params = boot_dev.format(img) 2755d676197SRobert Foley args = self._args + boot_params.split(' ') 2765d676197SRobert Foley args += self._data_args + extra_args + self._config['extra_args'] 277ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 278e56c4504SRobert Foley qemu_path = get_qemu_path(self.arch, self._build_path) 279*ff14ab0cSRobert Foley 280*ff14ab0cSRobert Foley # Since console_log_path is only set when the user provides the 281*ff14ab0cSRobert Foley # log_console option, we will set drain_console=True so the 282*ff14ab0cSRobert Foley # console is always drained. 283*ff14ab0cSRobert Foley guest = QEMUMachine(binary=qemu_path, args=args, 284*ff14ab0cSRobert Foley console_log=self._console_log_path, 285*ff14ab0cSRobert Foley drain_console=True) 2865d676197SRobert Foley guest.set_machine(self._config['machine']) 2878dd38334SGerd Hoffmann guest.set_console() 288ff2ebff0SFam Zheng try: 289ff2ebff0SFam Zheng guest.launch() 290ff2ebff0SFam Zheng except: 291ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 292e56c4504SRobert Foley logging.error(" ".join([qemu_path] + args)) 293ff2ebff0SFam Zheng logging.error("Log:") 294ff2ebff0SFam Zheng logging.error(guest.get_log()) 295ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 296ff2ebff0SFam Zheng raise 297ff2ebff0SFam Zheng atexit.register(self.shutdown) 298ff2ebff0SFam Zheng self._guest = guest 299*ff14ab0cSRobert Foley # Init console so we can start consuming the chars. 300*ff14ab0cSRobert Foley self.console_init() 301ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 302ff2ebff0SFam Zheng command_line="info usernet") 303ff2ebff0SFam Zheng self.ssh_port = None 304ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 305ff2ebff0SFam Zheng fields = l.split() 306ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 307ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 308ff2ebff0SFam Zheng if not self.ssh_port: 309ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 310ff2ebff0SFam Zheng usernet_info) 311ff2ebff0SFam Zheng 312*ff14ab0cSRobert Foley def console_init(self, timeout = None): 313*ff14ab0cSRobert Foley if timeout == None: 314*ff14ab0cSRobert Foley timeout = self.socket_timeout 3158dd38334SGerd Hoffmann vm = self._guest 3168dd38334SGerd Hoffmann vm.console_socket.settimeout(timeout) 317698a64f9SGerd Hoffmann self.console_raw_path = os.path.join(vm._temp_dir, 318698a64f9SGerd Hoffmann vm._name + "-console.raw") 319698a64f9SGerd Hoffmann self.console_raw_file = open(self.console_raw_path, 'wb') 3208dd38334SGerd Hoffmann 3218dd38334SGerd Hoffmann def console_log(self, text): 3228dd38334SGerd Hoffmann for line in re.split("[\r\n]", text): 3238dd38334SGerd Hoffmann # filter out terminal escape sequences 3248dd38334SGerd Hoffmann line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line) 3258dd38334SGerd Hoffmann line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line) 3268dd38334SGerd Hoffmann # replace unprintable chars 3278dd38334SGerd Hoffmann line = re.sub("\x1b", "<esc>", line) 3288dd38334SGerd Hoffmann line = re.sub("[\x00-\x1f]", ".", line) 3298dd38334SGerd Hoffmann line = re.sub("[\x80-\xff]", ".", line) 3308dd38334SGerd Hoffmann if line == "": 3318dd38334SGerd Hoffmann continue 3328dd38334SGerd Hoffmann # log console line 3338dd38334SGerd Hoffmann sys.stderr.write("con recv: %s\n" % line) 3348dd38334SGerd Hoffmann 33560136e06SGerd Hoffmann def console_wait(self, expect, expectalt = None): 3368dd38334SGerd Hoffmann vm = self._guest 3378dd38334SGerd Hoffmann output = "" 3388dd38334SGerd Hoffmann while True: 3398dd38334SGerd Hoffmann try: 3408dd38334SGerd Hoffmann chars = vm.console_socket.recv(1) 341698a64f9SGerd Hoffmann if self.console_raw_file: 342698a64f9SGerd Hoffmann self.console_raw_file.write(chars) 343698a64f9SGerd Hoffmann self.console_raw_file.flush() 3448dd38334SGerd Hoffmann except socket.timeout: 3458dd38334SGerd Hoffmann sys.stderr.write("console: *** read timeout ***\n") 3468dd38334SGerd Hoffmann sys.stderr.write("console: waiting for: '%s'\n" % expect) 34760136e06SGerd Hoffmann if not expectalt is None: 34860136e06SGerd Hoffmann sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 3498dd38334SGerd Hoffmann sys.stderr.write("console: line buffer:\n") 3508dd38334SGerd Hoffmann sys.stderr.write("\n") 3518dd38334SGerd Hoffmann self.console_log(output.rstrip()) 3528dd38334SGerd Hoffmann sys.stderr.write("\n") 3538dd38334SGerd Hoffmann raise 3548dd38334SGerd Hoffmann output += chars.decode("latin1") 3558dd38334SGerd Hoffmann if expect in output: 3568dd38334SGerd Hoffmann break 35760136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 35860136e06SGerd Hoffmann break 3598dd38334SGerd Hoffmann if "\r" in output or "\n" in output: 3608dd38334SGerd Hoffmann lines = re.split("[\r\n]", output) 3618dd38334SGerd Hoffmann output = lines.pop() 3628dd38334SGerd Hoffmann if self.debug: 3638dd38334SGerd Hoffmann self.console_log("\n".join(lines)) 3648dd38334SGerd Hoffmann if self.debug: 3658dd38334SGerd Hoffmann self.console_log(output) 36660136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 36760136e06SGerd Hoffmann return False 36860136e06SGerd Hoffmann return True 3698dd38334SGerd Hoffmann 3706c4f0416SGerd Hoffmann def console_consume(self): 3716c4f0416SGerd Hoffmann vm = self._guest 3726c4f0416SGerd Hoffmann output = "" 3736c4f0416SGerd Hoffmann vm.console_socket.setblocking(0) 3746c4f0416SGerd Hoffmann while True: 3756c4f0416SGerd Hoffmann try: 3766c4f0416SGerd Hoffmann chars = vm.console_socket.recv(1) 3776c4f0416SGerd Hoffmann except: 3786c4f0416SGerd Hoffmann break 3796c4f0416SGerd Hoffmann output += chars.decode("latin1") 3806c4f0416SGerd Hoffmann if "\r" in output or "\n" in output: 3816c4f0416SGerd Hoffmann lines = re.split("[\r\n]", output) 3826c4f0416SGerd Hoffmann output = lines.pop() 3836c4f0416SGerd Hoffmann if self.debug: 3846c4f0416SGerd Hoffmann self.console_log("\n".join(lines)) 3856c4f0416SGerd Hoffmann if self.debug: 3866c4f0416SGerd Hoffmann self.console_log(output) 3876c4f0416SGerd Hoffmann vm.console_socket.setblocking(1) 3886c4f0416SGerd Hoffmann 3898dd38334SGerd Hoffmann def console_send(self, command): 3908dd38334SGerd Hoffmann vm = self._guest 3918dd38334SGerd Hoffmann if self.debug: 3928dd38334SGerd Hoffmann logline = re.sub("\n", "<enter>", command) 3938dd38334SGerd Hoffmann logline = re.sub("[\x00-\x1f]", ".", logline) 3948dd38334SGerd Hoffmann sys.stderr.write("con send: %s\n" % logline) 3958dd38334SGerd Hoffmann for char in list(command): 3968dd38334SGerd Hoffmann vm.console_socket.send(char.encode("utf-8")) 3978dd38334SGerd Hoffmann time.sleep(0.01) 3988dd38334SGerd Hoffmann 3998dd38334SGerd Hoffmann def console_wait_send(self, wait, command): 4008dd38334SGerd Hoffmann self.console_wait(wait) 4018dd38334SGerd Hoffmann self.console_send(command) 4028dd38334SGerd Hoffmann 4038dd38334SGerd Hoffmann def console_ssh_init(self, prompt, user, pw): 4045d676197SRobert Foley sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 4055d676197SRobert Foley % self._config['ssh_pub_key'].rstrip() 4068dd38334SGerd Hoffmann self.console_wait_send("login:", "%s\n" % user) 4078dd38334SGerd Hoffmann self.console_wait_send("Password:", "%s\n" % pw) 4088dd38334SGerd Hoffmann self.console_wait_send(prompt, "mkdir .ssh\n") 4098dd38334SGerd Hoffmann self.console_wait_send(prompt, sshkey_cmd) 4108dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 755 .ssh\n") 4118dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 4128dd38334SGerd Hoffmann 4138dd38334SGerd Hoffmann def console_sshd_config(self, prompt): 4148dd38334SGerd Hoffmann self.console_wait(prompt) 4158dd38334SGerd Hoffmann self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 4168dd38334SGerd Hoffmann for var in self.envvars: 4178dd38334SGerd Hoffmann self.console_wait(prompt) 4188dd38334SGerd Hoffmann self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 4198dd38334SGerd Hoffmann 4208dd38334SGerd Hoffmann def print_step(self, text): 4218dd38334SGerd Hoffmann sys.stderr.write("### %s ...\n" % text) 4228dd38334SGerd Hoffmann 4236ee982c9SRobert Foley def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 424c9de3935SRobert Foley # Allow more time for VM to boot under TCG. 425c9de3935SRobert Foley if not kvm_available(self.arch): 426c9de3935SRobert Foley seconds *= self.tcg_ssh_timeout_multiplier 427ff2ebff0SFam Zheng starttime = datetime.datetime.now() 428f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 4296ee982c9SRobert Foley cmd_success = False 430f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 4316ee982c9SRobert Foley if wait_root and self.ssh_root(cmd) == 0: 4326ee982c9SRobert Foley cmd_success = True 433fbb3aa29SRobert Foley break 4346ee982c9SRobert Foley elif self.ssh(cmd) == 0: 4356ee982c9SRobert Foley cmd_success = True 436ff2ebff0SFam Zheng break 437f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 438f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 439ff2ebff0SFam Zheng time.sleep(1) 4406ee982c9SRobert Foley if not cmd_success: 441ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 442ff2ebff0SFam Zheng 443ff2ebff0SFam Zheng def shutdown(self): 444ff2ebff0SFam Zheng self._guest.shutdown() 445ff2ebff0SFam Zheng 446ff2ebff0SFam Zheng def wait(self): 447ff2ebff0SFam Zheng self._guest.wait() 448ff2ebff0SFam Zheng 449b3f94b2fSGerd Hoffmann def graceful_shutdown(self): 450b3f94b2fSGerd Hoffmann self.ssh_root(self.poweroff) 451b3f94b2fSGerd Hoffmann self._guest.wait() 452b3f94b2fSGerd Hoffmann 453ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 454ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 455ff2ebff0SFam Zheng 456b081986cSRobert Foley def gen_cloud_init_iso(self): 457b081986cSRobert Foley cidir = self._tmpdir 458b081986cSRobert Foley mdata = open(os.path.join(cidir, "meta-data"), "w") 459b081986cSRobert Foley name = self.name.replace(".","-") 460b081986cSRobert Foley mdata.writelines(["instance-id: {}-vm-0\n".format(name), 461b081986cSRobert Foley "local-hostname: {}-guest\n".format(name)]) 462b081986cSRobert Foley mdata.close() 463b081986cSRobert Foley udata = open(os.path.join(cidir, "user-data"), "w") 4645d676197SRobert Foley print("guest user:pw {}:{}".format(self._config['guest_user'], 4655d676197SRobert Foley self._config['guest_pass'])) 466b081986cSRobert Foley udata.writelines(["#cloud-config\n", 467b081986cSRobert Foley "chpasswd:\n", 468b081986cSRobert Foley " list: |\n", 4695d676197SRobert Foley " root:%s\n" % self._config['root_pass'], 4705d676197SRobert Foley " %s:%s\n" % (self._config['guest_user'], 4715d676197SRobert Foley self._config['guest_pass']), 472b081986cSRobert Foley " expire: False\n", 473b081986cSRobert Foley "users:\n", 4745d676197SRobert Foley " - name: %s\n" % self._config['guest_user'], 475b081986cSRobert Foley " sudo: ALL=(ALL) NOPASSWD:ALL\n", 476b081986cSRobert Foley " ssh-authorized-keys:\n", 4775d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 478b081986cSRobert Foley " - name: root\n", 479b081986cSRobert Foley " ssh-authorized-keys:\n", 4805d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 481b081986cSRobert Foley "locale: en_US.UTF-8\n"]) 482b081986cSRobert Foley proxy = os.environ.get("http_proxy") 483b081986cSRobert Foley if not proxy is None: 484b081986cSRobert Foley udata.writelines(["apt:\n", 485b081986cSRobert Foley " proxy: %s" % proxy]) 486b081986cSRobert Foley udata.close() 48792fecad3SAlex Bennée subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 488b081986cSRobert Foley "-volid", "cidata", "-joliet", "-rock", 489b081986cSRobert Foley "user-data", "meta-data"], 490b081986cSRobert Foley cwd=cidir, 491b081986cSRobert Foley stdin=self._devnull, stdout=self._stdout, 492b081986cSRobert Foley stderr=self._stdout) 493b081986cSRobert Foley return os.path.join(cidir, "cloud-init.iso") 494b081986cSRobert Foley 495e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None): 496e56c4504SRobert Foley """Fetch the path to the qemu binary.""" 497e56c4504SRobert Foley # If QEMU environment variable set, it takes precedence 498e56c4504SRobert Foley if "QEMU" in os.environ: 499e56c4504SRobert Foley qemu_path = os.environ["QEMU"] 500e56c4504SRobert Foley elif build_path: 501e56c4504SRobert Foley qemu_path = os.path.join(build_path, arch + "-softmmu") 502e56c4504SRobert Foley qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 503e56c4504SRobert Foley else: 504e56c4504SRobert Foley # Default is to use system path for qemu. 505e56c4504SRobert Foley qemu_path = "qemu-system-" + arch 506e56c4504SRobert Foley return qemu_path 507e56c4504SRobert Foley 50813336606SRobert Foleydef get_qemu_version(qemu_path): 50913336606SRobert Foley """Get the version number from the current QEMU, 51013336606SRobert Foley and return the major number.""" 51113336606SRobert Foley output = subprocess.check_output([qemu_path, '--version']) 51213336606SRobert Foley version_line = output.decode("utf-8") 51313336606SRobert Foley version_num = re.split(' |\(', version_line)[3].split('.')[0] 51413336606SRobert Foley return int(version_num) 51513336606SRobert Foley 5163f1e8137SRobert Foleydef parse_config(config, args): 5173f1e8137SRobert Foley """ Parse yaml config and populate our config structure. 5183f1e8137SRobert Foley The yaml config allows the user to override the 5193f1e8137SRobert Foley defaults for VM parameters. In many cases these 5203f1e8137SRobert Foley defaults can be overridden without rebuilding the VM.""" 5213f1e8137SRobert Foley if args.config: 5223f1e8137SRobert Foley config_file = args.config 5233f1e8137SRobert Foley elif 'QEMU_CONFIG' in os.environ: 5243f1e8137SRobert Foley config_file = os.environ['QEMU_CONFIG'] 5253f1e8137SRobert Foley else: 5263f1e8137SRobert Foley return config 5273f1e8137SRobert Foley if not os.path.exists(config_file): 5283f1e8137SRobert Foley raise Exception("config file {} does not exist".format(config_file)) 5293f1e8137SRobert Foley # We gracefully handle importing the yaml module 5303f1e8137SRobert Foley # since it might not be installed. 5313f1e8137SRobert Foley # If we are here it means the user supplied a .yml file, 5323f1e8137SRobert Foley # so if the yaml module is not installed we will exit with error. 5333f1e8137SRobert Foley try: 5343f1e8137SRobert Foley import yaml 5353f1e8137SRobert Foley except ImportError: 5363f1e8137SRobert Foley print("The python3-yaml package is needed "\ 5373f1e8137SRobert Foley "to support config.yaml files") 5383f1e8137SRobert Foley # Instead of raising an exception we exit to avoid 5393f1e8137SRobert Foley # a raft of messy (expected) errors to stdout. 5403f1e8137SRobert Foley exit(1) 5413f1e8137SRobert Foley with open(config_file) as f: 5423f1e8137SRobert Foley yaml_dict = yaml.safe_load(f) 5433f1e8137SRobert Foley 5443f1e8137SRobert Foley if 'qemu-conf' in yaml_dict: 5453f1e8137SRobert Foley config.update(yaml_dict['qemu-conf']) 5463f1e8137SRobert Foley else: 5473f1e8137SRobert Foley raise Exception("config file {} is not valid"\ 5483f1e8137SRobert Foley " missing qemu-conf".format(config_file)) 5493f1e8137SRobert Foley return config 5503f1e8137SRobert Foley 55163a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 5528a6e007eSPhilippe Mathieu-Daudé 5538a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 55463a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 5553ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 5568a6e007eSPhilippe Mathieu-Daudé else: 5578a6e007eSPhilippe Mathieu-Daudé return 1 5588a6e007eSPhilippe Mathieu-Daudé 559ff2ebff0SFam Zheng parser = optparse.OptionParser( 560ff2ebff0SFam Zheng description="VM test utility. Exit codes: " 561ff2ebff0SFam Zheng "0 = success, " 562ff2ebff0SFam Zheng "1 = command line error, " 563ff2ebff0SFam Zheng "2 = environment initialization failed, " 564ff2ebff0SFam Zheng "3 = test command failed") 565ff2ebff0SFam Zheng parser.add_option("--debug", "-D", action="store_true", 566ff2ebff0SFam Zheng help="enable debug output") 56763a24c5eSPhilippe Mathieu-Daudé parser.add_option("--image", "-i", default="%s.img" % vmcls.name, 568ff2ebff0SFam Zheng help="image file name") 569ff2ebff0SFam Zheng parser.add_option("--force", "-f", action="store_true", 570ff2ebff0SFam Zheng help="force build image even if image exists") 5718a6e007eSPhilippe Mathieu-Daudé parser.add_option("--jobs", type=int, default=get_default_jobs(), 572ff2ebff0SFam Zheng help="number of virtual CPUs") 57341e3340aSPeter Maydell parser.add_option("--verbose", "-V", action="store_true", 57441e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 575ff2ebff0SFam Zheng parser.add_option("--build-image", "-b", action="store_true", 576ff2ebff0SFam Zheng help="build image") 577ff2ebff0SFam Zheng parser.add_option("--build-qemu", 578ff2ebff0SFam Zheng help="build QEMU from source in guest") 5795c2ec9b6SAlex Bennée parser.add_option("--build-target", 5805c2ec9b6SAlex Bennée help="QEMU build target", default="check") 581e56c4504SRobert Foley parser.add_option("--build-path", default=None, 582e56c4504SRobert Foley help="Path of build directory, "\ 583e56c4504SRobert Foley "for using build tree QEMU binary. ") 584ff2ebff0SFam Zheng parser.add_option("--interactive", "-I", action="store_true", 585ff2ebff0SFam Zheng help="Interactively run command") 586983c2a77SFam Zheng parser.add_option("--snapshot", "-s", action="store_true", 587983c2a77SFam Zheng help="run tests with a snapshot") 58892fecad3SAlex Bennée parser.add_option("--genisoimage", default="genisoimage", 58992fecad3SAlex Bennée help="iso imaging tool") 5903f1e8137SRobert Foley parser.add_option("--config", "-c", default=None, 5913f1e8137SRobert Foley help="Provide config yaml for configuration. "\ 5923f1e8137SRobert Foley "See config_example.yaml for example.") 59313336606SRobert Foley parser.add_option("--efi-aarch64", 59413336606SRobert Foley default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 59513336606SRobert Foley help="Path to efi image for aarch64 VMs.") 596*ff14ab0cSRobert Foley parser.add_option("--log-console", action="store_true", 597*ff14ab0cSRobert Foley help="Log console to file.") 598ff2ebff0SFam Zheng parser.disable_interspersed_args() 599ff2ebff0SFam Zheng return parser.parse_args() 600ff2ebff0SFam Zheng 6015d676197SRobert Foleydef main(vmcls, config=None): 602ff2ebff0SFam Zheng try: 6035d676197SRobert Foley if config == None: 6045d676197SRobert Foley config = DEFAULT_CONFIG 60563a24c5eSPhilippe Mathieu-Daudé args, argv = parse_args(vmcls) 606ff2ebff0SFam Zheng if not argv and not args.build_qemu and not args.build_image: 607f03868bdSEduardo Habkost print("Nothing to do?") 608ff2ebff0SFam Zheng return 1 6093f1e8137SRobert Foley config = parse_config(config, args) 610fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 611fb3b4e6dSEduardo Habkost else logging.WARN)) 6125d676197SRobert Foley vm = vmcls(args, config=config) 613ff2ebff0SFam Zheng if args.build_image: 614ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 615ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 616ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 617ff2ebff0SFam Zheng return 1 618ff2ebff0SFam Zheng return vm.build_image(args.image) 619ff2ebff0SFam Zheng if args.build_qemu: 620ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 621ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 622ff2ebff0SFam Zheng configure_opts = " ".join(argv), 6233ace9be6SGerd Hoffmann jobs=int(args.jobs), 6245c2ec9b6SAlex Bennée target=args.build_target, 62541e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 626ff2ebff0SFam Zheng else: 627ff2ebff0SFam Zheng cmd = argv 628983c2a77SFam Zheng img = args.image 629983c2a77SFam Zheng if args.snapshot: 630983c2a77SFam Zheng img += ",snapshot=on" 631983c2a77SFam Zheng vm.boot(img) 632ff2ebff0SFam Zheng vm.wait_ssh() 633ff2ebff0SFam Zheng except Exception as e: 634ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 635ff2ebff0SFam Zheng return 0 636ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 637ff2ebff0SFam Zheng traceback.print_exc() 638ff2ebff0SFam Zheng return 2 639ff2ebff0SFam Zheng 640b3f94b2fSGerd Hoffmann exitcode = 0 641ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 642b3f94b2fSGerd Hoffmann exitcode = 3 643bcc388dfSAlex Bennée if args.interactive: 644b3f94b2fSGerd Hoffmann vm.ssh() 645b3f94b2fSGerd Hoffmann 646b3f94b2fSGerd Hoffmann if not args.snapshot: 647b3f94b2fSGerd Hoffmann vm.graceful_shutdown() 648b3f94b2fSGerd Hoffmann 649b3f94b2fSGerd Hoffmann return exitcode 650