summaryrefslogtreecommitdiffstats
path: root/src/ceph/qa/tasks/calamari_nosetests.py
blob: c6bbaf3630026f8d61c5cb923aac47f9c810782f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import contextlib
import logging
import os
import textwrap
import yaml

from cStringIO import StringIO
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:
        out = StringIO()
        # ensure branch is present (clone -b will succeed even if
        # the branch doesn't exist, falling back to master)
        client.run(
            args='git ls-remote %s %s' % (url, branch),
            stdout=out,
            label='check for calamari branch %s existence' % branch
        )
        if len(out.getvalue()) == 0:
            raise RuntimeError("Calamari branch %s doesn't exist" % branch)
        client.run(args='git clone -b %s %s' % (branch, url))
        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