diff --git a/tasks/calamari_nosetests.py b/tasks/calamari_nosetests.py new file mode 100644 index 00000000000..5c5b15dbecb --- /dev/null +++ b/tasks/calamari_nosetests.py @@ -0,0 +1,281 @@ +import contextlib +import logging +import os +import textwrap +import yaml + +from teuthology import contextutil +from teuthology import misc +from teuthology import packaging +from teuthology.orchestra import run + +log = logging.getLogger(__name__) + +# extra stuff we need to do our job here +EXTRA_PKGS = [ + 'git', +] + +# stuff that would be in a devmode install, but should be +# installed in the system for running nosetests against +# a production install. +EXTRA_NOSETEST_PKGS = [ + 'python-psutil', + 'python-mock', +] + + +def find_client0(cluster): + ''' Find remote that has client.0 role, or None ''' + for rem, roles in cluster.remotes.iteritems(): + if 'client.0' in roles: + return rem + return None + + +def pip(remote, package, venv=None, uninstall=False, force=False): + ''' {un}install a package with pip, possibly in a virtualenv ''' + if venv: + pip = os.path.join(venv, 'bin', 'pip') + args = ['sudo', pip] + else: + args = ['sudo', 'pip'] + + if uninstall: + args.extend(['uninstall', '-y']) + else: + args.append('install') + if force: + args.append('-I') + + args.append(package) + remote.run(args=args) + + +@contextlib.contextmanager +def install_epel(remote): + ''' install a disabled-by-default epel repo config file ''' + remove = False + try: + if remote.os.package_type == 'deb': + yield + else: + remove = True + distromajor = remote.os.version.split('.')[0] + + repofiledata = textwrap.dedent(''' + [epel] + name=epel{version} + metalink=http://mirrors.fedoraproject.org/metalink?repo=epel-{version}&arch=$basearch + enabled=0 + gpgcheck=0 + ''').format(version=distromajor) + + misc.create_file(remote, '/etc/yum.repos.d/epel.repo', + data=repofiledata, sudo=True) + remote.run(args='sudo yum clean all') + yield + + finally: + if remove: + misc.delete_file(remote, '/etc/yum.repos.d/epel.repo', sudo=True) + + +def enable_epel(remote, enable=True): + ''' enable/disable the epel repo ''' + args = 'sudo sed -i'.split() + if enable: + args.extend(['s/enabled=0/enabled=1/']) + else: + args.extend(['s/enabled=1/enabled=0/']) + args.extend(['/etc/yum.repos.d/epel.repo']) + + remote.run(args=args) + remote.run(args='sudo yum clean all') + + +@contextlib.contextmanager +def install_extra_pkgs(client): + ''' Install EXTRA_PKGS ''' + try: + for pkg in EXTRA_PKGS: + packaging.install_package(pkg, client) + yield + + finally: + for pkg in EXTRA_PKGS: + packaging.remove_package(pkg, client) + + +@contextlib.contextmanager +def clone_calamari(config, client): + ''' clone calamari source into current directory on remote ''' + branch = config.get('calamari_branch', 'master') + url = config.get('calamari_giturl', 'git://github.com/ceph/calamari') + try: + cmd = 'git clone -b {branch} {giturl}'.format( + branch=branch, giturl=url + ) + client.run(args=cmd) + yield + finally: + # sudo python setup.py develop may have left some root files around + client.run(args='sudo rm -rf calamari') + + +@contextlib.contextmanager +def write_info_yaml(cluster, client): + ''' write info.yaml to client for nosetests ''' + try: + info = { + 'cluster': { + rem.name: {'roles': roles} + for rem, roles in cluster.remotes.iteritems() + } + } + misc.create_file(client, 'calamari/info.yaml', + data=yaml.safe_dump(info, default_flow_style=False)) + yield + finally: + misc.delete_file(client, 'calamari/info.yaml') + + +@contextlib.contextmanager +def write_test_conf(client): + ''' write calamari/tests/test.conf to client for nosetests ''' + try: + testconf = textwrap.dedent(''' + [testing] + + calamari_control = external + ceph_control = external + bootstrap = False + api_username = admin + api_password = admin + embedded_timeout_factor = 1 + external_timeout_factor = 3 + external_cluster_path = info.yaml + ''') + misc.create_file(client, 'calamari/tests/test.conf', data=testconf) + yield + + finally: + misc.delete_file(client, 'calamari/tests/test.conf') + + +@contextlib.contextmanager +def prepare_nosetest_env(client): + try: + # extra dependencies that would be in the devmode venv + if client.os.package_type == 'rpm': + enable_epel(client, enable=True) + for package in EXTRA_NOSETEST_PKGS: + packaging.install_package(package, client) + if client.os.package_type == 'rpm': + enable_epel(client, enable=False) + + # install nose itself into the calamari venv, force it in case it's + # already installed in the system, so we can invoke it by path without + # fear that it's not present + pip(client, 'nose', venv='/opt/calamari/venv', force=True) + + # install a later version of requests into the venv as well + # (for precise) + pip(client, 'requests', venv='/opt/calamari/venv', force=True) + + # link (setup.py develop) calamari/rest-api into the production venv + # because production does not include calamari_rest.management, needed + # for test_rest_api.py's ApiIntrospection + args = 'cd calamari/rest-api'.split() + [run.Raw(';')] + \ + 'sudo /opt/calamari/venv/bin/python setup.py develop'.split() + client.run(args=args) + + # because, at least in Python 2.6/Centos, site.py uses + # 'os.path.exists()' to process .pth file entries, and exists() uses + # access(2) to check for existence, all the paths leading up to + # $HOME/calamari/rest-api need to be searchable by all users of + # the package, which will include the WSGI/Django app, running + # as the Apache user. So make them all world-read-and-execute. + args = 'sudo chmod a+x'.split() + \ + ['.', './calamari', './calamari/rest-api'] + client.run(args=args) + + # make one dummy request just to get the WSGI app to do + # all its log creation here, before the chmod below (I'm + # looking at you, graphite -- /var/log/calamari/info.log and + # /var/log/calamari/exception.log) + client.run(args='wget -q -O /dev/null http://localhost') + + # /var/log/calamari/* is root-or-apache write-only + client.run(args='sudo chmod a+w /var/log/calamari/*') + + yield + + finally: + args = 'cd calamari/rest-api'.split() + [run.Raw(';')] + \ + 'sudo /opt/calamari/venv/bin/python setup.py develop -u'.split() + client.run(args=args) + for pkg in ('nose', 'requests'): + pip(client, pkg, venv='/opt/calamari/venv', uninstall=True) + for package in EXTRA_NOSETEST_PKGS: + packaging.remove_package(package, client) + + +@contextlib.contextmanager +def run_nosetests(client): + ''' Actually run the tests ''' + args = [ + 'cd', + 'calamari', + run.Raw(';'), + 'CALAMARI_CONFIG=/etc/calamari/calamari.conf', + '/opt/calamari/venv/bin/nosetests', + '-v', + 'tests/', + ] + client.run(args=args) + yield + + +@contextlib.contextmanager +def task(ctx, config): + """ + Run Calamari tests against an instance set up by 'calamari_server'. + + -- clone the Calamari source into $HOME (see options) + -- write calamari/info.yaml describing the cluster + -- write calamari/tests/test.conf containing + 'external' for calamari_control and ceph_control + 'bootstrap = False' to disable test bootstrapping (installing minions) + no api_url necessary (inferred from client.0) + 'external_cluster_path = info.yaml' + -- modify the production Calamari install to allow test runs: + install nose in the venv + install EXTRA_NOSETEST_PKGS + link in, with setup.py develop, calamari_rest (for ApiIntrospection) + -- set CALAMARI_CONFIG to point to /etc/calamari/calamari.conf + -- nosetests -v tests/ + + Options are: + calamari_giturl: url from which to git clone calamari + (default: git://github.com/ceph/calamari) + calamari_branch: git branch of calamari to check out + (default: master) + + Note: the tests must find a clean cluster, so don't forget to + set the crush default type appropriately, or install min_size OSD hosts + """ + client0 = find_client0(ctx.cluster) + if client0 is None: + raise RuntimeError("must have client.0 role") + + with contextutil.nested( + lambda: install_epel(client0), + lambda: install_extra_pkgs(client0), + lambda: clone_calamari(config, client0), + lambda: write_info_yaml(ctx.cluster, client0), + lambda: write_test_conf(client0), + lambda: prepare_nosetest_env(client0), + lambda: run_nosetests(client0), + ): + yield