From patchwork Tue Sep 5 02:11:53 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Fam Zheng X-Patchwork-Id: 809897 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Authentication-Results: ozlabs.org; spf=pass (mailfrom) smtp.mailfrom=nongnu.org (client-ip=2001:4830:134:3::11; helo=lists.gnu.org; envelope-from=qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org; receiver=) Received: from lists.gnu.org (lists.gnu.org [IPv6:2001:4830:134:3::11]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 3xmVgm0Vyzz9s3T for ; Tue, 5 Sep 2017 12:16:04 +1000 (AEST) Received: from localhost ([::1]:56411 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1dp3PG-0005hi-4S for incoming@patchwork.ozlabs.org; Mon, 04 Sep 2017 22:16:02 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:60353) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1dp3M4-0003Hg-6K for qemu-devel@nongnu.org; Mon, 04 Sep 2017 22:12:49 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1dp3Ly-00082t-H2 for qemu-devel@nongnu.org; Mon, 04 Sep 2017 22:12:44 -0400 Received: from mx1.redhat.com ([209.132.183.28]:56086) by eggs.gnu.org with esmtps (TLS1.0:DHE_RSA_AES_256_CBC_SHA1:32) (Exim 4.71) (envelope-from ) id 1dp3Ly-00082T-7q for qemu-devel@nongnu.org; Mon, 04 Sep 2017 22:12:38 -0400 Received: from smtp.corp.redhat.com (int-mx02.intmail.prod.int.phx2.redhat.com [10.5.11.12]) (using TLSv1.2 with cipher AECDH-AES256-SHA (256/256 bits)) (No client certificate requested) by mx1.redhat.com (Postfix) with ESMTPS id 5842D285C1; Tue, 5 Sep 2017 02:12:37 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.3.2 mx1.redhat.com 5842D285C1 Authentication-Results: ext-mx06.extmail.prod.ext.phx2.redhat.com; dmarc=none (p=none dis=none) header.from=redhat.com Authentication-Results: ext-mx06.extmail.prod.ext.phx2.redhat.com; spf=fail smtp.mailfrom=famz@redhat.com Received: from lemon.redhat.com (ovpn-12-97.pek2.redhat.com [10.72.12.97]) by smtp.corp.redhat.com (Postfix) with ESMTP id 7B5A37F7E4; Tue, 5 Sep 2017 02:12:29 +0000 (UTC) From: Fam Zheng To: qemu-devel@nongnu.org Date: Tue, 5 Sep 2017 10:11:53 +0800 Message-Id: <20170905021201.25684-5-famz@redhat.com> In-Reply-To: <20170905021201.25684-1-famz@redhat.com> References: <20170905021201.25684-1-famz@redhat.com> X-Scanned-By: MIMEDefang 2.79 on 10.5.11.12 X-Greylist: Sender IP whitelisted, not delayed by milter-greylist-4.5.16 (mx1.redhat.com [10.5.110.30]); Tue, 05 Sep 2017 02:12:37 +0000 (UTC) X-detected-operating-system: by eggs.gnu.org: GNU/Linux 2.2.x-3.x [generic] [fuzzy] X-Received-From: 209.132.183.28 Subject: [Qemu-devel] [PATCH v6 04/12] tests: Add vm test lib X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Cc: Peter Maydell , Fam Zheng , Cleber Rosa , =?utf-8?q?Philippe_Mathieu-Daud=C3=A9?= , Kamil Rytarowski , stefanha@redhat.com, pbonzini@redhat.com, =?utf-8?q?Alex_Benn=C3=A9e?= Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: "Qemu-devel" This is the common code to implement a "VM test" to 1) Download and initialize a pre-defined VM that has necessary dependencies to build QEMU and SSH access. 2) Archive $SRC_PATH to a .tar file. 3) Boot the VM, and pass the source tar file to the guest. 4) SSH into the VM, untar the source tarball, build from the source. Signed-off-by: Fam Zheng --- tests/vm/basevm.py | 276 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100755 tests/vm/basevm.py diff --git a/tests/vm/basevm.py b/tests/vm/basevm.py new file mode 100755 index 0000000000..9db91d61fa --- /dev/null +++ b/tests/vm/basevm.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python +# +# VM testing base class +# +# Copyright (C) 2017 Red Hat Inc. +# +# Authors: +# Fam Zheng +# +# This work is licensed under the terms of the GNU GPL, version 2. See +# the COPYING file in the top-level directory. +# + +import os +import sys +import logging +import time +import datetime +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "..", "scripts")) +from qemu import QEMUMachine +import subprocess +import hashlib +import optparse +import atexit +import tempfile +import shutil +import multiprocessing +import traceback + +SSH_KEY = """\ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAopAuOlmLV6LVHdFBj8/eeOwI9CqguIJPp7eAQSZvOiB4Ag/R +coEhl/RBbrV5Yc/SmSD4PTpJO/iM10RwliNjDb4a3I8q3sykRJu9c9PI/YsH8WN9 ++NH2NjKPtJIcKTu287IM5JYxyB6nDoOzILbTyJ1TDR/xH6qYEfBAyiblggdjcvhA +RTf93QIn39F/xLypXvT1K2O9BJEsnJ8lEUvB2UXhKo/JTfSeZF8wPBeowaP9EONk +7b+nuJOWHGg68Ji6wVi62tjwl2Szch6lxIhZBpnV7QNRKMfYHP6eIyF4pusazzZq +Telsq6xI2ghecWLzb/MF5A+rklsGx2FNuJSAJwIDAQABAoIBAHHi4o/8VZNivz0x +cWXn8erzKV6tUoWQvW85Lj/2RiwJvSlsnYZDkx5af1CpEE2HA/pFT8PNRqsd+MWC +7AEy710cVsM4BYerBFYQaYxwzblaoojo88LSjVPw3h5Z0iLM8+IMVd36nwuc9dpE +R8TecMZ1+U4Tl6BgqkK+9xToZRdPKdjS8L5MoFhGN+xY0vRbbJbGaV9Q0IHxLBkB +rEBV7T1mUynneCHRUQlJQEwJmKpT8MH3IjsUXlG5YvnuuvcQJSNTaW2iDLxuOKp8 +cxW8+qL88zpb1D5dppoIu6rlrugN0azSq70ruFJQPc/A8GQrDKoGgRQiagxNY3u+ +vHZzXlECgYEA0dKO3gfkSxsDBb94sQwskMScqLhcKhztEa8kPxTx6Yqh+x8/scx3 +XhJyOt669P8U1v8a/2Al+s81oZzzfQSzO1Q7gEwSrgBcRMSIoRBUw9uYcy02ngb/ +j/ng3DGivfJztjjiSJwb46FHkJ2JR8mF2UisC6UMXk3NgFY/3vWQx78CgYEAxlcG +T3hfSWSmTgKRczMJuHQOX9ULfTBIqwP5VqkkkiavzigGRirzb5lgnmuTSPTpF0LB +XVPjR2M4q+7gzP0Dca3pocrvLEoxjwIKnCbYKnyyvnUoE9qHv4Kr+vDbgWpa2LXG +JbLmE7tgTCIp20jOPPT4xuDvlbzQZBJ5qCQSoZkCgYEAgrotSSihlCnAOFSTXbu4 +CHp3IKe8xIBBNENq0eK61kcJpOxTQvOha3sSsJsU4JAM6+cFaxb8kseHIqonCj1j +bhOM/uJmwQJ4el/4wGDsbxriYOBKpyq1D38gGhDS1IW6kk3erl6VAb36WJ/OaGum +eTpN9vNeQWM4Jj2WjdNx4QECgYAwTdd6mU1TmZCrJRL5ZG+0nYc2rbMrnQvFoqUi +BvWiJovggHzur90zy73tNzPaq9Ls2FQxf5G1vCN8NCRJqEEjeYCR59OSDMu/EXc2 +CnvQ9SevHOdS1oEDEjcCWZCMFzPi3XpRih1gptzQDe31uuiHjf3cqcGPzTlPdfRt +D8P92QKBgC4UaBvIRwREVJsdZzpIzm224Bpe8LOmA7DeTnjlT0b3lkGiBJ36/Q0p +VhYh/6cjX4/iuIs7gJbGon7B+YPB8scmOi3fj0+nkJAONue1mMfBNkba6qQTc6Y2 +5mEKw2/O7/JpND7ucU3OK9plcw/qnrWDgHxl0Iz95+OzUIIagxne +-----END RSA PRIVATE KEY----- +""" +SSH_PUB_KEY = """\ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCikC46WYtXotUd0UGPz9547Aj0KqC4gk+nt4BBJm86IHgCD9FygSGX9EFutXlhz9KZIPg9Okk7+IzXRHCWI2MNvhrcjyrezKREm71z08j9iwfxY3340fY2Mo+0khwpO7bzsgzkljHIHqcOg7MgttPInVMNH/EfqpgR8EDKJuWCB2Ny+EBFN/3dAiff0X/EvKle9PUrY70EkSycnyURS8HZReEqj8lN9J5kXzA8F6jBo/0Q42Ttv6e4k5YcaDrwmLrBWLra2PCXZLNyHqXEiFkGmdXtA1Eox9gc/p4jIXim6xrPNmpN6WyrrEjaCF5xYvNv8wXkD6uSWwbHYU24lIAn qemu-vm-key +""" + +class BaseVM(object): + GUEST_USER = "qemu" + GUEST_PASS = "qemupass" + ROOT_PASS = "qemupass" + + # The script to run in the guest that builds QEMU + BUILD_SCRIPT = "" + # The guest name, to be overridden by subclasses + name = "#base" + def __init__(self, debug=False, vcpus=None): + self._guest = None + self._tmpdir = tempfile.mkdtemp(prefix="vm-test-", suffix=".tmp", dir=".") + atexit.register(shutil.rmtree, self._tmpdir) + + self._ssh_key_file = os.path.join(self._tmpdir, "id_rsa") + open(self._ssh_key_file, "w").write(SSH_KEY) + subprocess.check_call(["chmod", "600", self._ssh_key_file]) + + self._ssh_pub_key_file = os.path.join(self._tmpdir, "id_rsa.pub") + open(self._ssh_pub_key_file, "w").write(SSH_PUB_KEY) + + self.debug = debug + self._stderr = sys.stderr + self._devnull = open("/dev/null", "w") + if self.debug: + self._stdout = sys.stdout + else: + self._stdout = self._devnull + self._args = [ \ + "-nodefaults", "-m", "2G", + "-cpu", "host", + "-netdev", "user,id=vnet,hostfwd=:0.0.0.0:0-:22", + "-device", "virtio-net-pci,netdev=vnet", + "-vnc", ":0,to=20", + "-serial", "file:%s" % os.path.join(self._tmpdir, "serial.out")] + if vcpus: + self._args += ["-smp", str(vcpus)] + if os.access("/dev/kvm", os.R_OK | os.W_OK): + self._args += ["-enable-kvm"] + else: + logging.info("KVM not available, not using -enable-kvm") + self._data_args = [] + + def _download_with_cache(self, url, sha256sum=None): + def check_sha256sum(fname): + if not sha256sum: + return True + checksum = subprocess.check_output(["sha256sum", fname]).split()[0] + return sha256sum == checksum + + cache_dir = os.path.expanduser("~/.cache/qemu-vm/download") + if not os.path.exists(cache_dir): + os.makedirs(cache_dir) + fname = os.path.join(cache_dir, hashlib.sha1(url).hexdigest()) + if os.path.exists(fname) and check_sha256sum(fname): + return fname + logging.debug("Downloading %s to %s...", url, fname) + subprocess.check_call(["wget", "-c", url, "-O", fname + ".download"], + stdout=self._stdout, stderr=self._stderr) + os.rename(fname + ".download", fname) + return fname + + def _ssh_do(self, user, cmd, check, interactive=False): + ssh_cmd = ["ssh", "-q", + "-o", "StrictHostKeyChecking=no", + "-o", "UserKnownHostsFile=/dev/null", + "-o", "ConnectTimeout=1", + "-p", self.ssh_port, "-i", self._ssh_key_file] + if interactive: + ssh_cmd += ['-t'] + assert not isinstance(cmd, str) + ssh_cmd += ["%s@127.0.0.1" % user] + list(cmd) + logging.debug("ssh_cmd: %s", " ".join(ssh_cmd)) + r = subprocess.call(ssh_cmd, + stdin=sys.stdin if interactive else self._devnull, + stdout=sys.stdout if interactive else self._stdout, + stderr=sys.stderr if interactive else self._stderr) + if check and r != 0: + raise Exception("SSH command failed: %s" % cmd) + return r + + def ssh(self, *cmd): + return self._ssh_do(self.GUEST_USER, cmd, False) + + def ssh_interactive(self, *cmd): + return self._ssh_do(self.GUEST_USER, cmd, False, True) + + def ssh_root(self, *cmd): + return self._ssh_do("root", cmd, False) + + def ssh_check(self, *cmd): + self._ssh_do(self.GUEST_USER, cmd, True) + + def ssh_root_check(self, *cmd): + self._ssh_do("root", cmd, True) + + def build_image(self, img): + raise NotImplementedError + + def add_source_dir(self, src_dir): + name = "data-" + hashlib.sha1(src_dir).hexdigest()[:5] + tarfile = os.path.join(self._tmpdir, name + ".tar") + logging.debug("Creating archive %s for src_dir dir: %s", tarfile, src_dir) + subprocess.check_call(["./scripts/archive-source.sh", tarfile], + cwd=src_dir, stdin=self._devnull, + stdout=self._stdout, stderr=self._stderr) + self._data_args += ["-drive", + "file=%s,if=none,id=%s,cache=writeback,format=raw" % \ + (tarfile, name), + "-device", + "virtio-blk,drive=%s,serial=%s,bootindex=1" % (name, name)] + + def boot(self, img, extra_args=[]): + args = self._args + [ + "-device", "VGA", + "-drive", "file=%s,if=none,id=drive0,cache=writeback" % img, + "-device", "virtio-blk,drive=drive0,bootindex=0"] + args += self._data_args + extra_args + logging.debug("QEMU args: %s", " ".join(args)) + guest = QEMUMachine(binary=os.environ.get("QEMU", "qemu-system-x86_64"), + args=args) + guest.launch() + atexit.register(self.shutdown) + self._guest = guest + usernet_info = guest.qmp("human-monitor-command", + command_line="info usernet") + self.ssh_port = None + for l in usernet_info["return"].splitlines(): + fields = l.split() + if "TCP[HOST_FORWARD]" in fields and "22" in fields: + self.ssh_port = l.split()[3] + if not self.ssh_port: + raise Exception("Cannot find ssh port from 'info usernet':\n%s" % \ + usernet_info) + + def wait_ssh(self, seconds=120): + starttime = datetime.datetime.now() + guest_up = False + while (datetime.datetime.now() - starttime).total_seconds() < seconds: + if self.ssh("exit 0") == 0: + guest_up = True + break + time.sleep(1) + if not guest_up: + raise Exception("Timeout while waiting for guest ssh") + + def shutdown(self): + self._guest.shutdown() + + def wait(self): + self._guest.wait() + + def qmp(self, *args, **kwargs): + return self._guest.qmp(*args, **kwargs) + +def parse_args(vm_name): + parser = optparse.OptionParser(description=""" + VM test utility. Exit codes: 0 = success, 1 = command line error, 2 = environment initialization failed, 3 = test command failed""") + parser.add_option("--debug", "-D", action="store_true", + help="enable debug output") + parser.add_option("--image", "-i", default="%s.img" % vm_name, + help="image file name") + parser.add_option("--force", "-f", action="store_true", + help="force build image even if image exists") + parser.add_option("--jobs", type=int, default=multiprocessing.cpu_count() / 2, + help="number of virtual CPUs") + parser.add_option("--build-image", "-b", action="store_true", + help="build image") + parser.add_option("--build-qemu", + help="build QEMU from source in guest") + parser.add_option("--interactive", "-I", action="store_true", + help="Interactively run command") + parser.disable_interspersed_args() + return parser.parse_args() + +def main(vmcls): + try: + args, argv = parse_args(vmcls.name) + if not argv and not args.build_qemu and not args.build_image: + print "Nothing to do?" + return 1 + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + vm = vmcls(debug=args.debug, vcpus=args.jobs) + if args.build_image: + if os.path.exists(args.image) and not args.force: + sys.stderr.writelines(["Image file exists: %s\n" % args.image, + "Use --force option to overwrite\n"]) + return 1 + return vm.build_image(args.image) + if args.build_qemu: + vm.add_source_dir(args.build_qemu) + cmd = [vm.BUILD_SCRIPT.format( + configure_opts = " ".join(argv), + jobs=args.jobs)] + else: + cmd = argv + vm.boot(args.image + ",snapshot=on") + vm.wait_ssh() + except Exception as e: + if isinstance(e, SystemExit) and e.code == 0: + return 0 + sys.stderr.write("Failed to prepare guest environment\n") + traceback.print_exc() + return 2 + + if args.interactive: + if vm.ssh_interactive(*cmd) == 0: + return 0 + vm.ssh_interactive() + return 3 + else: + if vm.ssh(*cmd) != 0: + return 3