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 262fea3a12SAlex Bennéeimport argparse 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", 47*9fc33bf4SAlexander von Gluck IV 'root_user' : "root", 485d676197SRobert Foley 'root_pass' : "qemupass", 495d676197SRobert Foley 'ssh_key_file' : SSH_KEY_FILE, 505d676197SRobert Foley 'ssh_pub_key_file': SSH_PUB_KEY_FILE, 515d676197SRobert Foley 'memory' : "4G", 525d676197SRobert Foley 'extra_args' : [], 535d676197SRobert Foley 'qemu_args' : "", 545d676197SRobert Foley 'dns' : "", 555d676197SRobert Foley 'ssh_port' : 0, 565d676197SRobert Foley 'install_cmds' : "", 575d676197SRobert Foley 'boot_dev_type' : "block", 585d676197SRobert Foley 'ssh_timeout' : 1, 595d676197SRobert Foley} 605d676197SRobert FoleyBOOT_DEVICE = { 615d676197SRobert Foley 'block' : "-drive file={},if=none,id=drive0,cache=writeback "\ 625d676197SRobert Foley "-device virtio-blk,drive=drive0,bootindex=0", 635d676197SRobert Foley 'scsi' : "-device virtio-scsi-device,id=scsi "\ 645d676197SRobert Foley "-drive file={},format=raw,if=none,id=hd0 "\ 655d676197SRobert Foley "-device scsi-hd,drive=hd0,bootindex=0", 665d676197SRobert Foley} 67ff2ebff0SFam Zhengclass BaseVM(object): 68ff2ebff0SFam Zheng 69b08ba163SGerd Hoffmann envvars = [ 70b08ba163SGerd Hoffmann "https_proxy", 71b08ba163SGerd Hoffmann "http_proxy", 72b08ba163SGerd Hoffmann "ftp_proxy", 73b08ba163SGerd Hoffmann "no_proxy", 74b08ba163SGerd Hoffmann ] 75b08ba163SGerd Hoffmann 76ff2ebff0SFam Zheng # The script to run in the guest that builds QEMU 77ff2ebff0SFam Zheng BUILD_SCRIPT = "" 78ff2ebff0SFam Zheng # The guest name, to be overridden by subclasses 79ff2ebff0SFam Zheng name = "#base" 8031719c37SPhilippe Mathieu-Daudé # The guest architecture, to be overridden by subclasses 8131719c37SPhilippe Mathieu-Daudé arch = "#arch" 82b3f94b2fSGerd Hoffmann # command to halt the guest, can be overridden by subclasses 83b3f94b2fSGerd Hoffmann poweroff = "poweroff" 844a70232bSRobert Foley # Time to wait for shutdown to finish. 854a70232bSRobert Foley shutdown_timeout_default = 30 865b790481SEduardo Habkost # enable IPv6 networking 875b790481SEduardo Habkost ipv6 = True 885d676197SRobert Foley # This is the timeout on the wait for console bytes. 895d676197SRobert Foley socket_timeout = 120 90c9de3935SRobert Foley # Scale up some timeouts under TCG. 91c9de3935SRobert Foley # 4 is arbitrary, but greater than 2, 92c9de3935SRobert Foley # since we found we need to wait more than twice as long. 934a70232bSRobert Foley tcg_timeout_multiplier = 4 945d676197SRobert Foley def __init__(self, args, config=None): 95ff2ebff0SFam Zheng self._guest = None 961f335d18SRobert Foley self._genisoimage = args.genisoimage 971f335d18SRobert Foley self._build_path = args.build_path 9813336606SRobert Foley self._efi_aarch64 = args.efi_aarch64 995d676197SRobert Foley # Allow input config to override defaults. 1005d676197SRobert Foley self._config = DEFAULT_CONFIG.copy() 1015d676197SRobert Foley if config != None: 1025d676197SRobert Foley self._config.update(config) 1035d676197SRobert Foley self.validate_ssh_keys() 104ff2ebff0SFam Zheng self._tmpdir = os.path.realpath(tempfile.mkdtemp(prefix="vm-test-", 105ff2ebff0SFam Zheng suffix=".tmp", 106ff2ebff0SFam Zheng dir=".")) 107ff2ebff0SFam Zheng atexit.register(shutil.rmtree, self._tmpdir) 1085d676197SRobert Foley # Copy the key files to a temporary directory. 1095d676197SRobert Foley # Also chmod the key file to agree with ssh requirements. 1105d676197SRobert Foley self._config['ssh_key'] = \ 1115d676197SRobert Foley open(self._config['ssh_key_file']).read().rstrip() 1125d676197SRobert Foley self._config['ssh_pub_key'] = \ 1135d676197SRobert Foley open(self._config['ssh_pub_key_file']).read().rstrip() 1145d676197SRobert Foley self._ssh_tmp_key_file = os.path.join(self._tmpdir, "id_rsa") 1155d676197SRobert Foley open(self._ssh_tmp_key_file, "w").write(self._config['ssh_key']) 1165d676197SRobert Foley subprocess.check_call(["chmod", "600", self._ssh_tmp_key_file]) 117ff2ebff0SFam Zheng 1185d676197SRobert Foley self._ssh_tmp_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") 1195d676197SRobert Foley open(self._ssh_tmp_pub_key_file, 1205d676197SRobert Foley "w").write(self._config['ssh_pub_key']) 121ff2ebff0SFam Zheng 1221f335d18SRobert Foley self.debug = args.debug 123ff14ab0cSRobert Foley self._console_log_path = None 124ff14ab0cSRobert Foley if args.log_console: 125ff14ab0cSRobert Foley self._console_log_path = \ 126ff14ab0cSRobert Foley os.path.join(os.path.expanduser("~/.cache/qemu-vm"), 127ff14ab0cSRobert Foley "{}.install.log".format(self.name)) 128ff2ebff0SFam Zheng self._stderr = sys.stderr 129ff2ebff0SFam Zheng self._devnull = open(os.devnull, "w") 130ff2ebff0SFam Zheng if self.debug: 131ff2ebff0SFam Zheng self._stdout = sys.stdout 132ff2ebff0SFam Zheng else: 133ff2ebff0SFam Zheng self._stdout = self._devnull 1345d676197SRobert Foley netdev = "user,id=vnet,hostfwd=:127.0.0.1:{}-:22" 135ff2ebff0SFam Zheng self._args = [ \ 1365d676197SRobert Foley "-nodefaults", "-m", self._config['memory'], 1375d676197SRobert Foley "-cpu", self._config['cpu'], 1385d676197SRobert Foley "-netdev", 1395d676197SRobert Foley netdev.format(self._config['ssh_port']) + 1405d676197SRobert Foley (",ipv6=no" if not self.ipv6 else "") + 1415d676197SRobert Foley (",dns=" + self._config['dns'] if self._config['dns'] else ""), 142ff2ebff0SFam Zheng "-device", "virtio-net-pci,netdev=vnet", 1438dd38334SGerd Hoffmann "-vnc", "127.0.0.1:0,to=20"] 1441f335d18SRobert Foley if args.jobs and args.jobs > 1: 1451f335d18SRobert Foley self._args += ["-smp", "%d" % args.jobs] 14671531bb5SPhilippe Mathieu-Daudé if kvm_available(self.arch): 1474a70232bSRobert Foley self._shutdown_timeout = self.shutdown_timeout_default 148ff2ebff0SFam Zheng self._args += ["-enable-kvm"] 149ff2ebff0SFam Zheng else: 150ff2ebff0SFam Zheng logging.info("KVM not available, not using -enable-kvm") 1514a70232bSRobert Foley self._shutdown_timeout = \ 1524a70232bSRobert Foley self.shutdown_timeout_default * self.tcg_timeout_multiplier 153ff2ebff0SFam Zheng self._data_args = [] 154ff2ebff0SFam Zheng 1555d676197SRobert Foley if self._config['qemu_args'] != None: 1565d676197SRobert Foley qemu_args = self._config['qemu_args'] 1575d676197SRobert Foley qemu_args = qemu_args.replace('\n',' ').replace('\r','') 1585d676197SRobert Foley # shlex groups quoted arguments together 1595d676197SRobert Foley # we need this to keep the quoted args together for when 1605d676197SRobert Foley # the QEMU command is issued later. 1615d676197SRobert Foley args = shlex.split(qemu_args) 1625d676197SRobert Foley self._config['extra_args'] = [] 1635d676197SRobert Foley for arg in args: 1645d676197SRobert Foley if arg: 1655d676197SRobert Foley # Preserve quotes around arguments. 1665d676197SRobert Foley # shlex above takes them out, so add them in. 1675d676197SRobert Foley if " " in arg: 1685d676197SRobert Foley arg = '"{}"'.format(arg) 1695d676197SRobert Foley self._config['extra_args'].append(arg) 1705d676197SRobert Foley 1715d676197SRobert Foley def validate_ssh_keys(self): 1725d676197SRobert Foley """Check to see if the ssh key files exist.""" 1735d676197SRobert Foley if 'ssh_key_file' not in self._config or\ 1745d676197SRobert Foley not os.path.exists(self._config['ssh_key_file']): 1755d676197SRobert Foley raise Exception("ssh key file not found.") 1765d676197SRobert Foley if 'ssh_pub_key_file' not in self._config or\ 1775d676197SRobert Foley not os.path.exists(self._config['ssh_pub_key_file']): 1785d676197SRobert Foley raise Exception("ssh pub key file not found.") 1795d676197SRobert Foley 1805d676197SRobert Foley def wait_boot(self, wait_string=None): 1815d676197SRobert Foley """Wait for the standard string we expect 1825d676197SRobert Foley on completion of a normal boot. 1835d676197SRobert Foley The user can also choose to override with an 1845d676197SRobert Foley alternate string to wait for.""" 1855d676197SRobert Foley if wait_string is None: 1865d676197SRobert Foley if self.login_prompt is None: 1875d676197SRobert Foley raise Exception("self.login_prompt not defined") 1885d676197SRobert Foley wait_string = self.login_prompt 1895d676197SRobert Foley # Intentionally bump up the default timeout under TCG, 1905d676197SRobert Foley # since the console wait below takes longer. 1915d676197SRobert Foley timeout = self.socket_timeout 1925d676197SRobert Foley if not kvm_available(self.arch): 1935d676197SRobert Foley timeout *= 8 1945d676197SRobert Foley self.console_init(timeout=timeout) 1955d676197SRobert Foley self.console_wait(wait_string) 1965d676197SRobert Foley 1975b4b4865SAlex Bennée def _download_with_cache(self, url, sha256sum=None, sha512sum=None): 198ff2ebff0SFam Zheng def check_sha256sum(fname): 199ff2ebff0SFam Zheng if not sha256sum: 200ff2ebff0SFam Zheng return True 201ff2ebff0SFam Zheng checksum = subprocess.check_output(["sha256sum", fname]).split()[0] 2023ace9be6SGerd Hoffmann return sha256sum == checksum.decode("utf-8") 203ff2ebff0SFam Zheng 2045b4b4865SAlex Bennée def check_sha512sum(fname): 2055b4b4865SAlex Bennée if not sha512sum: 2065b4b4865SAlex Bennée return True 2075b4b4865SAlex Bennée checksum = subprocess.check_output(["sha512sum", fname]).split()[0] 2085b4b4865SAlex Bennée return sha512sum == checksum.decode("utf-8") 2095b4b4865SAlex Bennée 210ff2ebff0SFam Zheng cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") 211ff2ebff0SFam Zheng if not os.path.exists(cache_dir): 212ff2ebff0SFam Zheng os.makedirs(cache_dir) 2133ace9be6SGerd Hoffmann fname = os.path.join(cache_dir, 2143ace9be6SGerd Hoffmann hashlib.sha1(url.encode("utf-8")).hexdigest()) 2155b4b4865SAlex Bennée if os.path.exists(fname) and check_sha256sum(fname) and check_sha512sum(fname): 216ff2ebff0SFam Zheng return fname 217ff2ebff0SFam Zheng logging.debug("Downloading %s to %s...", url, fname) 218ff2ebff0SFam Zheng subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], 219ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 220ff2ebff0SFam Zheng os.rename(fname + ".download", fname) 221ff2ebff0SFam Zheng return fname 222ff2ebff0SFam Zheng 223796471e9SGerd Hoffmann def _ssh_do(self, user, cmd, check): 22489adc5b9SRobert Foley ssh_cmd = ["ssh", 22589adc5b9SRobert Foley "-t", 226ff2ebff0SFam Zheng "-o", "StrictHostKeyChecking=no", 227ff2ebff0SFam Zheng "-o", "UserKnownHostsFile=" + os.devnull, 2285d676197SRobert Foley "-o", 2295d676197SRobert Foley "ConnectTimeout={}".format(self._config["ssh_timeout"]), 2305d676197SRobert Foley "-p", self.ssh_port, "-i", self._ssh_tmp_key_file] 23189adc5b9SRobert Foley # If not in debug mode, set ssh to quiet mode to 23289adc5b9SRobert Foley # avoid printing the results of commands. 23389adc5b9SRobert Foley if not self.debug: 23489adc5b9SRobert Foley ssh_cmd.append("-q") 235b08ba163SGerd Hoffmann for var in self.envvars: 236b08ba163SGerd Hoffmann ssh_cmd += ['-o', "SendEnv=%s" % var ] 237ff2ebff0SFam Zheng assert not isinstance(cmd, str) 238ff2ebff0SFam Zheng ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) 239ff2ebff0SFam Zheng logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) 240726c9a3bSFam Zheng r = subprocess.call(ssh_cmd) 241ff2ebff0SFam Zheng if check and r != 0: 242ff2ebff0SFam Zheng raise Exception("SSH command failed: %s" % cmd) 243ff2ebff0SFam Zheng return r 244ff2ebff0SFam Zheng 245ff2ebff0SFam Zheng def ssh(self, *cmd): 246df001680SRobert Foley return self._ssh_do(self._config["guest_user"], cmd, False) 247ff2ebff0SFam Zheng 248ff2ebff0SFam Zheng def ssh_root(self, *cmd): 249*9fc33bf4SAlexander von Gluck IV return self._ssh_do(self._config["root_user"], cmd, False) 250ff2ebff0SFam Zheng 251ff2ebff0SFam Zheng def ssh_check(self, *cmd): 252df001680SRobert Foley self._ssh_do(self._config["guest_user"], cmd, True) 253ff2ebff0SFam Zheng 254ff2ebff0SFam Zheng def ssh_root_check(self, *cmd): 255*9fc33bf4SAlexander von Gluck IV self._ssh_do(self._config["root_user"], cmd, True) 256ff2ebff0SFam Zheng 257ff2ebff0SFam Zheng def build_image(self, img): 258ff2ebff0SFam Zheng raise NotImplementedError 259ff2ebff0SFam Zheng 2601e48931cSWainer dos Santos Moschetta def exec_qemu_img(self, *args): 2611e48931cSWainer dos Santos Moschetta cmd = [os.environ.get("QEMU_IMG", "qemu-img")] 2621e48931cSWainer dos Santos Moschetta cmd.extend(list(args)) 2631e48931cSWainer dos Santos Moschetta subprocess.check_call(cmd) 2641e48931cSWainer dos Santos Moschetta 265ff2ebff0SFam Zheng def add_source_dir(self, src_dir): 2663ace9be6SGerd Hoffmann name = "data-" + hashlib.sha1(src_dir.encode("utf-8")).hexdigest()[:5] 267ff2ebff0SFam Zheng tarfile = os.path.join(self._tmpdir, name + ".tar") 268ff2ebff0SFam Zheng logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) 269ff2ebff0SFam Zheng subprocess.check_call(["./scripts/archive-source.sh", tarfile], 270ff2ebff0SFam Zheng cwd=src_dir, stdin=self._devnull, 271ff2ebff0SFam Zheng stdout=self._stdout, stderr=self._stderr) 272ff2ebff0SFam Zheng self._data_args += ["-drive", 273ff2ebff0SFam Zheng "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ 274ff2ebff0SFam Zheng (tarfile, name), 275ff2ebff0SFam Zheng "-device", 276ff2ebff0SFam Zheng "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] 277ff2ebff0SFam Zheng 278ff2ebff0SFam Zheng def boot(self, img, extra_args=[]): 2795d676197SRobert Foley boot_dev = BOOT_DEVICE[self._config['boot_dev_type']] 2805d676197SRobert Foley boot_params = boot_dev.format(img) 2815d676197SRobert Foley args = self._args + boot_params.split(' ') 2825d676197SRobert Foley args += self._data_args + extra_args + self._config['extra_args'] 283ff2ebff0SFam Zheng logging.debug("QEMU args: %s", " ".join(args)) 284e56c4504SRobert Foley qemu_path = get_qemu_path(self.arch, self._build_path) 285ff14ab0cSRobert Foley 286ff14ab0cSRobert Foley # Since console_log_path is only set when the user provides the 287ff14ab0cSRobert Foley # log_console option, we will set drain_console=True so the 288ff14ab0cSRobert Foley # console is always drained. 289ff14ab0cSRobert Foley guest = QEMUMachine(binary=qemu_path, args=args, 290ff14ab0cSRobert Foley console_log=self._console_log_path, 291ff14ab0cSRobert Foley drain_console=True) 2925d676197SRobert Foley guest.set_machine(self._config['machine']) 2938dd38334SGerd Hoffmann guest.set_console() 294ff2ebff0SFam Zheng try: 295ff2ebff0SFam Zheng guest.launch() 296ff2ebff0SFam Zheng except: 297ff2ebff0SFam Zheng logging.error("Failed to launch QEMU, command line:") 298e56c4504SRobert Foley logging.error(" ".join([qemu_path] + args)) 299ff2ebff0SFam Zheng logging.error("Log:") 300ff2ebff0SFam Zheng logging.error(guest.get_log()) 301ff2ebff0SFam Zheng logging.error("QEMU version >= 2.10 is required") 302ff2ebff0SFam Zheng raise 303ff2ebff0SFam Zheng atexit.register(self.shutdown) 304ff2ebff0SFam Zheng self._guest = guest 305ff14ab0cSRobert Foley # Init console so we can start consuming the chars. 306ff14ab0cSRobert Foley self.console_init() 307ff2ebff0SFam Zheng usernet_info = guest.qmp("human-monitor-command", 308ff2ebff0SFam Zheng command_line="info usernet") 309ff2ebff0SFam Zheng self.ssh_port = None 310ff2ebff0SFam Zheng for l in usernet_info["return"].splitlines(): 311ff2ebff0SFam Zheng fields = l.split() 312ff2ebff0SFam Zheng if "TCP[HOST_FORWARD]" in fields and "22" in fields: 313ff2ebff0SFam Zheng self.ssh_port = l.split()[3] 314ff2ebff0SFam Zheng if not self.ssh_port: 315ff2ebff0SFam Zheng raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ 316ff2ebff0SFam Zheng usernet_info) 317ff2ebff0SFam Zheng 318ff14ab0cSRobert Foley def console_init(self, timeout = None): 319ff14ab0cSRobert Foley if timeout == None: 320ff14ab0cSRobert Foley timeout = self.socket_timeout 3218dd38334SGerd Hoffmann vm = self._guest 3228dd38334SGerd Hoffmann vm.console_socket.settimeout(timeout) 323698a64f9SGerd Hoffmann self.console_raw_path = os.path.join(vm._temp_dir, 324698a64f9SGerd Hoffmann vm._name + "-console.raw") 325698a64f9SGerd Hoffmann self.console_raw_file = open(self.console_raw_path, 'wb') 3268dd38334SGerd Hoffmann 3278dd38334SGerd Hoffmann def console_log(self, text): 3288dd38334SGerd Hoffmann for line in re.split("[\r\n]", text): 3298dd38334SGerd Hoffmann # filter out terminal escape sequences 3308dd38334SGerd Hoffmann line = re.sub("\x1b\[[0-9;?]*[a-zA-Z]", "", line) 3318dd38334SGerd Hoffmann line = re.sub("\x1b\([0-9;?]*[a-zA-Z]", "", line) 3328dd38334SGerd Hoffmann # replace unprintable chars 3338dd38334SGerd Hoffmann line = re.sub("\x1b", "<esc>", line) 3348dd38334SGerd Hoffmann line = re.sub("[\x00-\x1f]", ".", line) 3358dd38334SGerd Hoffmann line = re.sub("[\x80-\xff]", ".", line) 3368dd38334SGerd Hoffmann if line == "": 3378dd38334SGerd Hoffmann continue 3388dd38334SGerd Hoffmann # log console line 3398dd38334SGerd Hoffmann sys.stderr.write("con recv: %s\n" % line) 3408dd38334SGerd Hoffmann 34160136e06SGerd Hoffmann def console_wait(self, expect, expectalt = None): 3428dd38334SGerd Hoffmann vm = self._guest 3438dd38334SGerd Hoffmann output = "" 3448dd38334SGerd Hoffmann while True: 3458dd38334SGerd Hoffmann try: 3468dd38334SGerd Hoffmann chars = vm.console_socket.recv(1) 347698a64f9SGerd Hoffmann if self.console_raw_file: 348698a64f9SGerd Hoffmann self.console_raw_file.write(chars) 349698a64f9SGerd Hoffmann self.console_raw_file.flush() 3508dd38334SGerd Hoffmann except socket.timeout: 3518dd38334SGerd Hoffmann sys.stderr.write("console: *** read timeout ***\n") 3528dd38334SGerd Hoffmann sys.stderr.write("console: waiting for: '%s'\n" % expect) 35360136e06SGerd Hoffmann if not expectalt is None: 35460136e06SGerd Hoffmann sys.stderr.write("console: waiting for: '%s' (alt)\n" % expectalt) 3558dd38334SGerd Hoffmann sys.stderr.write("console: line buffer:\n") 3568dd38334SGerd Hoffmann sys.stderr.write("\n") 3578dd38334SGerd Hoffmann self.console_log(output.rstrip()) 3588dd38334SGerd Hoffmann sys.stderr.write("\n") 3598dd38334SGerd Hoffmann raise 3608dd38334SGerd Hoffmann output += chars.decode("latin1") 3618dd38334SGerd Hoffmann if expect in output: 3628dd38334SGerd Hoffmann break 36360136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 36460136e06SGerd Hoffmann break 3658dd38334SGerd Hoffmann if "\r" in output or "\n" in output: 3668dd38334SGerd Hoffmann lines = re.split("[\r\n]", output) 3678dd38334SGerd Hoffmann output = lines.pop() 3688dd38334SGerd Hoffmann if self.debug: 3698dd38334SGerd Hoffmann self.console_log("\n".join(lines)) 3708dd38334SGerd Hoffmann if self.debug: 3718dd38334SGerd Hoffmann self.console_log(output) 37260136e06SGerd Hoffmann if not expectalt is None and expectalt in output: 37360136e06SGerd Hoffmann return False 37460136e06SGerd Hoffmann return True 3758dd38334SGerd Hoffmann 3766c4f0416SGerd Hoffmann def console_consume(self): 3776c4f0416SGerd Hoffmann vm = self._guest 3786c4f0416SGerd Hoffmann output = "" 3796c4f0416SGerd Hoffmann vm.console_socket.setblocking(0) 3806c4f0416SGerd Hoffmann while True: 3816c4f0416SGerd Hoffmann try: 3826c4f0416SGerd Hoffmann chars = vm.console_socket.recv(1) 3836c4f0416SGerd Hoffmann except: 3846c4f0416SGerd Hoffmann break 3856c4f0416SGerd Hoffmann output += chars.decode("latin1") 3866c4f0416SGerd Hoffmann if "\r" in output or "\n" in output: 3876c4f0416SGerd Hoffmann lines = re.split("[\r\n]", output) 3886c4f0416SGerd Hoffmann output = lines.pop() 3896c4f0416SGerd Hoffmann if self.debug: 3906c4f0416SGerd Hoffmann self.console_log("\n".join(lines)) 3916c4f0416SGerd Hoffmann if self.debug: 3926c4f0416SGerd Hoffmann self.console_log(output) 3936c4f0416SGerd Hoffmann vm.console_socket.setblocking(1) 3946c4f0416SGerd Hoffmann 3958dd38334SGerd Hoffmann def console_send(self, command): 3968dd38334SGerd Hoffmann vm = self._guest 3978dd38334SGerd Hoffmann if self.debug: 3988dd38334SGerd Hoffmann logline = re.sub("\n", "<enter>", command) 3998dd38334SGerd Hoffmann logline = re.sub("[\x00-\x1f]", ".", logline) 4008dd38334SGerd Hoffmann sys.stderr.write("con send: %s\n" % logline) 4018dd38334SGerd Hoffmann for char in list(command): 4028dd38334SGerd Hoffmann vm.console_socket.send(char.encode("utf-8")) 4038dd38334SGerd Hoffmann time.sleep(0.01) 4048dd38334SGerd Hoffmann 4058dd38334SGerd Hoffmann def console_wait_send(self, wait, command): 4068dd38334SGerd Hoffmann self.console_wait(wait) 4078dd38334SGerd Hoffmann self.console_send(command) 4088dd38334SGerd Hoffmann 4098dd38334SGerd Hoffmann def console_ssh_init(self, prompt, user, pw): 4105d676197SRobert Foley sshkey_cmd = "echo '%s' > .ssh/authorized_keys\n" \ 4115d676197SRobert Foley % self._config['ssh_pub_key'].rstrip() 4128dd38334SGerd Hoffmann self.console_wait_send("login:", "%s\n" % user) 4138dd38334SGerd Hoffmann self.console_wait_send("Password:", "%s\n" % pw) 4148dd38334SGerd Hoffmann self.console_wait_send(prompt, "mkdir .ssh\n") 4158dd38334SGerd Hoffmann self.console_wait_send(prompt, sshkey_cmd) 4168dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 755 .ssh\n") 4178dd38334SGerd Hoffmann self.console_wait_send(prompt, "chmod 644 .ssh/authorized_keys\n") 4188dd38334SGerd Hoffmann 4198dd38334SGerd Hoffmann def console_sshd_config(self, prompt): 4208dd38334SGerd Hoffmann self.console_wait(prompt) 4218dd38334SGerd Hoffmann self.console_send("echo 'PermitRootLogin yes' >> /etc/ssh/sshd_config\n") 4228dd38334SGerd Hoffmann for var in self.envvars: 4238dd38334SGerd Hoffmann self.console_wait(prompt) 4248dd38334SGerd Hoffmann self.console_send("echo 'AcceptEnv %s' >> /etc/ssh/sshd_config\n" % var) 4258dd38334SGerd Hoffmann 4268dd38334SGerd Hoffmann def print_step(self, text): 4278dd38334SGerd Hoffmann sys.stderr.write("### %s ...\n" % text) 4288dd38334SGerd Hoffmann 4296ee982c9SRobert Foley def wait_ssh(self, wait_root=False, seconds=300, cmd="exit 0"): 430c9de3935SRobert Foley # Allow more time for VM to boot under TCG. 431c9de3935SRobert Foley if not kvm_available(self.arch): 4324a70232bSRobert Foley seconds *= self.tcg_timeout_multiplier 433ff2ebff0SFam Zheng starttime = datetime.datetime.now() 434f5d3d218SPhilippe Mathieu-Daudé endtime = starttime + datetime.timedelta(seconds=seconds) 4356ee982c9SRobert Foley cmd_success = False 436f5d3d218SPhilippe Mathieu-Daudé while datetime.datetime.now() < endtime: 4376ee982c9SRobert Foley if wait_root and self.ssh_root(cmd) == 0: 4386ee982c9SRobert Foley cmd_success = True 439fbb3aa29SRobert Foley break 4406ee982c9SRobert Foley elif self.ssh(cmd) == 0: 4416ee982c9SRobert Foley cmd_success = True 442ff2ebff0SFam Zheng break 443f5d3d218SPhilippe Mathieu-Daudé seconds = (endtime - datetime.datetime.now()).total_seconds() 444f5d3d218SPhilippe Mathieu-Daudé logging.debug("%ds before timeout", seconds) 445ff2ebff0SFam Zheng time.sleep(1) 4466ee982c9SRobert Foley if not cmd_success: 447ff2ebff0SFam Zheng raise Exception("Timeout while waiting for guest ssh") 448ff2ebff0SFam Zheng 449ff2ebff0SFam Zheng def shutdown(self): 4504a70232bSRobert Foley self._guest.shutdown(timeout=self._shutdown_timeout) 451ff2ebff0SFam Zheng 452ff2ebff0SFam Zheng def wait(self): 4534a70232bSRobert Foley self._guest.wait(timeout=self._shutdown_timeout) 454ff2ebff0SFam Zheng 455b3f94b2fSGerd Hoffmann def graceful_shutdown(self): 456b3f94b2fSGerd Hoffmann self.ssh_root(self.poweroff) 4574a70232bSRobert Foley self._guest.wait(timeout=self._shutdown_timeout) 458b3f94b2fSGerd Hoffmann 459ff2ebff0SFam Zheng def qmp(self, *args, **kwargs): 460ff2ebff0SFam Zheng return self._guest.qmp(*args, **kwargs) 461ff2ebff0SFam Zheng 462b081986cSRobert Foley def gen_cloud_init_iso(self): 463b081986cSRobert Foley cidir = self._tmpdir 464b081986cSRobert Foley mdata = open(os.path.join(cidir, "meta-data"), "w") 465b081986cSRobert Foley name = self.name.replace(".","-") 466b081986cSRobert Foley mdata.writelines(["instance-id: {}-vm-0\n".format(name), 467b081986cSRobert Foley "local-hostname: {}-guest\n".format(name)]) 468b081986cSRobert Foley mdata.close() 469b081986cSRobert Foley udata = open(os.path.join(cidir, "user-data"), "w") 4705d676197SRobert Foley print("guest user:pw {}:{}".format(self._config['guest_user'], 4715d676197SRobert Foley self._config['guest_pass'])) 472b081986cSRobert Foley udata.writelines(["#cloud-config\n", 473b081986cSRobert Foley "chpasswd:\n", 474b081986cSRobert Foley " list: |\n", 4755d676197SRobert Foley " root:%s\n" % self._config['root_pass'], 4765d676197SRobert Foley " %s:%s\n" % (self._config['guest_user'], 4775d676197SRobert Foley self._config['guest_pass']), 478b081986cSRobert Foley " expire: False\n", 479b081986cSRobert Foley "users:\n", 4805d676197SRobert Foley " - name: %s\n" % self._config['guest_user'], 481b081986cSRobert Foley " sudo: ALL=(ALL) NOPASSWD:ALL\n", 482b081986cSRobert Foley " ssh-authorized-keys:\n", 4835d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 484b081986cSRobert Foley " - name: root\n", 485b081986cSRobert Foley " ssh-authorized-keys:\n", 4865d676197SRobert Foley " - %s\n" % self._config['ssh_pub_key'], 487b081986cSRobert Foley "locale: en_US.UTF-8\n"]) 488b081986cSRobert Foley proxy = os.environ.get("http_proxy") 489b081986cSRobert Foley if not proxy is None: 490b081986cSRobert Foley udata.writelines(["apt:\n", 491b081986cSRobert Foley " proxy: %s" % proxy]) 492b081986cSRobert Foley udata.close() 49392fecad3SAlex Bennée subprocess.check_call([self._genisoimage, "-output", "cloud-init.iso", 494b081986cSRobert Foley "-volid", "cidata", "-joliet", "-rock", 495b081986cSRobert Foley "user-data", "meta-data"], 496b081986cSRobert Foley cwd=cidir, 497b081986cSRobert Foley stdin=self._devnull, stdout=self._stdout, 498b081986cSRobert Foley stderr=self._stdout) 499b081986cSRobert Foley return os.path.join(cidir, "cloud-init.iso") 500b081986cSRobert Foley 501e56c4504SRobert Foleydef get_qemu_path(arch, build_path=None): 502e56c4504SRobert Foley """Fetch the path to the qemu binary.""" 503e56c4504SRobert Foley # If QEMU environment variable set, it takes precedence 504e56c4504SRobert Foley if "QEMU" in os.environ: 505e56c4504SRobert Foley qemu_path = os.environ["QEMU"] 506e56c4504SRobert Foley elif build_path: 507e56c4504SRobert Foley qemu_path = os.path.join(build_path, arch + "-softmmu") 508e56c4504SRobert Foley qemu_path = os.path.join(qemu_path, "qemu-system-" + arch) 509e56c4504SRobert Foley else: 510e56c4504SRobert Foley # Default is to use system path for qemu. 511e56c4504SRobert Foley qemu_path = "qemu-system-" + arch 512e56c4504SRobert Foley return qemu_path 513e56c4504SRobert Foley 51413336606SRobert Foleydef get_qemu_version(qemu_path): 51513336606SRobert Foley """Get the version number from the current QEMU, 51613336606SRobert Foley and return the major number.""" 51713336606SRobert Foley output = subprocess.check_output([qemu_path, '--version']) 51813336606SRobert Foley version_line = output.decode("utf-8") 51913336606SRobert Foley version_num = re.split(' |\(', version_line)[3].split('.')[0] 52013336606SRobert Foley return int(version_num) 52113336606SRobert Foley 5223f1e8137SRobert Foleydef parse_config(config, args): 5233f1e8137SRobert Foley """ Parse yaml config and populate our config structure. 5243f1e8137SRobert Foley The yaml config allows the user to override the 5253f1e8137SRobert Foley defaults for VM parameters. In many cases these 5263f1e8137SRobert Foley defaults can be overridden without rebuilding the VM.""" 5273f1e8137SRobert Foley if args.config: 5283f1e8137SRobert Foley config_file = args.config 5293f1e8137SRobert Foley elif 'QEMU_CONFIG' in os.environ: 5303f1e8137SRobert Foley config_file = os.environ['QEMU_CONFIG'] 5313f1e8137SRobert Foley else: 5323f1e8137SRobert Foley return config 5333f1e8137SRobert Foley if not os.path.exists(config_file): 5343f1e8137SRobert Foley raise Exception("config file {} does not exist".format(config_file)) 5353f1e8137SRobert Foley # We gracefully handle importing the yaml module 5363f1e8137SRobert Foley # since it might not be installed. 5373f1e8137SRobert Foley # If we are here it means the user supplied a .yml file, 5383f1e8137SRobert Foley # so if the yaml module is not installed we will exit with error. 5393f1e8137SRobert Foley try: 5403f1e8137SRobert Foley import yaml 5413f1e8137SRobert Foley except ImportError: 5423f1e8137SRobert Foley print("The python3-yaml package is needed "\ 5433f1e8137SRobert Foley "to support config.yaml files") 5443f1e8137SRobert Foley # Instead of raising an exception we exit to avoid 5453f1e8137SRobert Foley # a raft of messy (expected) errors to stdout. 5463f1e8137SRobert Foley exit(1) 5473f1e8137SRobert Foley with open(config_file) as f: 5483f1e8137SRobert Foley yaml_dict = yaml.safe_load(f) 5493f1e8137SRobert Foley 5503f1e8137SRobert Foley if 'qemu-conf' in yaml_dict: 5513f1e8137SRobert Foley config.update(yaml_dict['qemu-conf']) 5523f1e8137SRobert Foley else: 5533f1e8137SRobert Foley raise Exception("config file {} is not valid"\ 5543f1e8137SRobert Foley " missing qemu-conf".format(config_file)) 5553f1e8137SRobert Foley return config 5563f1e8137SRobert Foley 55763a24c5eSPhilippe Mathieu-Daudédef parse_args(vmcls): 5588a6e007eSPhilippe Mathieu-Daudé 5598a6e007eSPhilippe Mathieu-Daudé def get_default_jobs(): 560b0953944SAlex Bennée if multiprocessing.cpu_count() > 1: 56163a24c5eSPhilippe Mathieu-Daudé if kvm_available(vmcls.arch): 5623ad3e36eSWainer dos Santos Moschetta return multiprocessing.cpu_count() // 2 563b0953944SAlex Bennée elif os.uname().machine == "x86_64" and \ 564b0953944SAlex Bennée vmcls.arch in ["aarch64", "x86_64", "i386"]: 565b0953944SAlex Bennée # MTTCG is available on these arches and we can allow 566b0953944SAlex Bennée # more cores. but only up to a reasonable limit. User 567b0953944SAlex Bennée # can always override these limits with --jobs. 568b0953944SAlex Bennée return min(multiprocessing.cpu_count() // 2, 8) 5698a6e007eSPhilippe Mathieu-Daudé else: 5708a6e007eSPhilippe Mathieu-Daudé return 1 5718a6e007eSPhilippe Mathieu-Daudé 5722fea3a12SAlex Bennée parser = argparse.ArgumentParser( 5732fea3a12SAlex Bennée formatter_class=argparse.ArgumentDefaultsHelpFormatter, 5742fea3a12SAlex Bennée description="Utility for provisioning VMs and running builds", 5752fea3a12SAlex Bennée epilog="""Remaining arguments are passed to the command. 5762fea3a12SAlex Bennée Exit codes: 0 = success, 1 = command line error, 5772fea3a12SAlex Bennée 2 = environment initialization failed, 5782fea3a12SAlex Bennée 3 = test command failed""") 5792fea3a12SAlex Bennée parser.add_argument("--debug", "-D", action="store_true", 580ff2ebff0SFam Zheng help="enable debug output") 5812fea3a12SAlex Bennée parser.add_argument("--image", "-i", default="%s.img" % vmcls.name, 582ff2ebff0SFam Zheng help="image file name") 5832fea3a12SAlex Bennée parser.add_argument("--force", "-f", action="store_true", 584ff2ebff0SFam Zheng help="force build image even if image exists") 5852fea3a12SAlex Bennée parser.add_argument("--jobs", type=int, default=get_default_jobs(), 586ff2ebff0SFam Zheng help="number of virtual CPUs") 5872fea3a12SAlex Bennée parser.add_argument("--verbose", "-V", action="store_true", 58841e3340aSPeter Maydell help="Pass V=1 to builds within the guest") 5892fea3a12SAlex Bennée parser.add_argument("--build-image", "-b", action="store_true", 590ff2ebff0SFam Zheng help="build image") 5912fea3a12SAlex Bennée parser.add_argument("--build-qemu", 592ff2ebff0SFam Zheng help="build QEMU from source in guest") 5932fea3a12SAlex Bennée parser.add_argument("--build-target", 5945c2ec9b6SAlex Bennée help="QEMU build target", default="check") 5952fea3a12SAlex Bennée parser.add_argument("--build-path", default=None, 596e56c4504SRobert Foley help="Path of build directory, "\ 597e56c4504SRobert Foley "for using build tree QEMU binary. ") 5982fea3a12SAlex Bennée parser.add_argument("--interactive", "-I", action="store_true", 599ff2ebff0SFam Zheng help="Interactively run command") 6002fea3a12SAlex Bennée parser.add_argument("--snapshot", "-s", action="store_true", 601983c2a77SFam Zheng help="run tests with a snapshot") 6022fea3a12SAlex Bennée parser.add_argument("--genisoimage", default="genisoimage", 60392fecad3SAlex Bennée help="iso imaging tool") 6042fea3a12SAlex Bennée parser.add_argument("--config", "-c", default=None, 6053f1e8137SRobert Foley help="Provide config yaml for configuration. "\ 6063f1e8137SRobert Foley "See config_example.yaml for example.") 6072fea3a12SAlex Bennée parser.add_argument("--efi-aarch64", 60813336606SRobert Foley default="/usr/share/qemu-efi-aarch64/QEMU_EFI.fd", 60913336606SRobert Foley help="Path to efi image for aarch64 VMs.") 6102fea3a12SAlex Bennée parser.add_argument("--log-console", action="store_true", 611ff14ab0cSRobert Foley help="Log console to file.") 6122fea3a12SAlex Bennée parser.add_argument("commands", nargs="*", help="""Remaining 6132fea3a12SAlex Bennée commands after -- are passed to command inside the VM""") 6142fea3a12SAlex Bennée 615ff2ebff0SFam Zheng return parser.parse_args() 616ff2ebff0SFam Zheng 6175d676197SRobert Foleydef main(vmcls, config=None): 618ff2ebff0SFam Zheng try: 6195d676197SRobert Foley if config == None: 6205d676197SRobert Foley config = DEFAULT_CONFIG 6212fea3a12SAlex Bennée args = parse_args(vmcls) 6222fea3a12SAlex Bennée if not args.commands and not args.build_qemu and not args.build_image: 623f03868bdSEduardo Habkost print("Nothing to do?") 624ff2ebff0SFam Zheng return 1 6253f1e8137SRobert Foley config = parse_config(config, args) 626fb3b4e6dSEduardo Habkost logging.basicConfig(level=(logging.DEBUG if args.debug 627fb3b4e6dSEduardo Habkost else logging.WARN)) 6285d676197SRobert Foley vm = vmcls(args, config=config) 629ff2ebff0SFam Zheng if args.build_image: 630ff2ebff0SFam Zheng if os.path.exists(args.image) and not args.force: 631ff2ebff0SFam Zheng sys.stderr.writelines(["Image file exists: %s\n" % args.image, 632ff2ebff0SFam Zheng "Use --force option to overwrite\n"]) 633ff2ebff0SFam Zheng return 1 634ff2ebff0SFam Zheng return vm.build_image(args.image) 635ff2ebff0SFam Zheng if args.build_qemu: 636ff2ebff0SFam Zheng vm.add_source_dir(args.build_qemu) 637ff2ebff0SFam Zheng cmd = [vm.BUILD_SCRIPT.format( 6382fea3a12SAlex Bennée configure_opts = " ".join(args.commands), 6393ace9be6SGerd Hoffmann jobs=int(args.jobs), 6405c2ec9b6SAlex Bennée target=args.build_target, 64141e3340aSPeter Maydell verbose = "V=1" if args.verbose else "")] 642ff2ebff0SFam Zheng else: 6432fea3a12SAlex Bennée cmd = args.commands 644983c2a77SFam Zheng img = args.image 645983c2a77SFam Zheng if args.snapshot: 646983c2a77SFam Zheng img += ",snapshot=on" 647983c2a77SFam Zheng vm.boot(img) 648ff2ebff0SFam Zheng vm.wait_ssh() 649ff2ebff0SFam Zheng except Exception as e: 650ff2ebff0SFam Zheng if isinstance(e, SystemExit) and e.code == 0: 651ff2ebff0SFam Zheng return 0 652ff2ebff0SFam Zheng sys.stderr.write("Failed to prepare guest environment\n") 653ff2ebff0SFam Zheng traceback.print_exc() 654ff2ebff0SFam Zheng return 2 655ff2ebff0SFam Zheng 656b3f94b2fSGerd Hoffmann exitcode = 0 657ff2ebff0SFam Zheng if vm.ssh(*cmd) != 0: 658b3f94b2fSGerd Hoffmann exitcode = 3 659bcc388dfSAlex Bennée if args.interactive: 660b3f94b2fSGerd Hoffmann vm.ssh() 661b3f94b2fSGerd Hoffmann 662b3f94b2fSGerd Hoffmann if not args.snapshot: 663b3f94b2fSGerd Hoffmann vm.graceful_shutdown() 664b3f94b2fSGerd Hoffmann 665b3f94b2fSGerd Hoffmann return exitcode 666