]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/s3tests.py
d/control: depend on python3-yaml for ceph-mgr
[ceph.git] / ceph / qa / tasks / s3tests.py
CommitLineData
7c673cae
FG
1"""
2Run a set of s3 tests on rgw.
3"""
4from cStringIO import StringIO
5from configobj import ConfigObj
6import base64
7import contextlib
8import logging
9import os
10import random
11import string
12
7c673cae
FG
13from teuthology import misc as teuthology
14from teuthology import contextutil
15from teuthology.config import config as teuth_config
16from teuthology.orchestra import run
9f95a23c 17from teuthology.exceptions import ConfigError
7c673cae
FG
18
19log = logging.getLogger(__name__)
20
7c673cae
FG
21@contextlib.contextmanager
22def download(ctx, config):
23 """
24 Download the s3 tests from the git builder.
25 Remove downloaded s3 file upon exit.
26
27 The context passed in should be identical to the context
28 passed in to the main task.
29 """
30 assert isinstance(config, dict)
31 log.info('Downloading s3-tests...')
32 testdir = teuthology.get_testdir(ctx)
92f5a8d4
TL
33 for (client, client_config) in config.items():
34 s3tests_branch = client_config.get('force-branch', None)
35 if not s3tests_branch:
7c673cae 36 raise ValueError(
92f5a8d4
TL
37 "Could not determine what branch to use for s3-tests. Please add 'force-branch: {s3-tests branch name}' to the .yaml config for this s3tests task.")
38
39 log.info("Using branch '%s' for s3tests", s3tests_branch)
40 sha1 = client_config.get('sha1')
41 git_remote = client_config.get('git_remote', teuth_config.ceph_git_base_url)
7c673cae
FG
42 ctx.cluster.only(client).run(
43 args=[
44 'git', 'clone',
92f5a8d4 45 '-b', s3tests_branch,
31f18b77 46 git_remote + 's3-tests.git',
7c673cae
FG
47 '{tdir}/s3-tests'.format(tdir=testdir),
48 ],
49 )
50 if sha1 is not None:
51 ctx.cluster.only(client).run(
52 args=[
53 'cd', '{tdir}/s3-tests'.format(tdir=testdir),
54 run.Raw('&&'),
55 'git', 'reset', '--hard', sha1,
56 ],
57 )
58 try:
59 yield
60 finally:
61 log.info('Removing s3-tests...')
62 testdir = teuthology.get_testdir(ctx)
63 for client in config:
64 ctx.cluster.only(client).run(
65 args=[
66 'rm',
67 '-rf',
68 '{tdir}/s3-tests'.format(tdir=testdir),
69 ],
70 )
71
72
73def _config_user(s3tests_conf, section, user):
74 """
75 Configure users for this section by stashing away keys, ids, and
76 email addresses.
77 """
78 s3tests_conf[section].setdefault('user_id', user)
79 s3tests_conf[section].setdefault('email', '{user}+test@test.test'.format(user=user))
80 s3tests_conf[section].setdefault('display_name', 'Mr. {user}'.format(user=user))
9f95a23c 81 s3tests_conf[section].setdefault('access_key', ''.join(random.choice(string.uppercase) for i in range(20)))
7c673cae 82 s3tests_conf[section].setdefault('secret_key', base64.b64encode(os.urandom(40)))
9f95a23c 83 s3tests_conf[section].setdefault('totp_serial', ''.join(random.choice(string.digits) for i in range(10)))
11fdf7f2
TL
84 s3tests_conf[section].setdefault('totp_seed', base64.b32encode(os.urandom(40)))
85 s3tests_conf[section].setdefault('totp_seconds', '5')
7c673cae
FG
86
87
88@contextlib.contextmanager
89def create_users(ctx, config):
90 """
91 Create a main and an alternate s3 user.
92 """
93 assert isinstance(config, dict)
94 log.info('Creating rgw users...')
95 testdir = teuthology.get_testdir(ctx)
31f18b77 96 users = {'s3 main': 'foo', 's3 alt': 'bar', 's3 tenant': 'testx$tenanteduser'}
7c673cae
FG
97 for client in config['clients']:
98 s3tests_conf = config['s3tests_conf'][client]
99 s3tests_conf.setdefault('fixtures', {})
100 s3tests_conf['fixtures'].setdefault('bucket prefix', 'test-' + client + '-{random}-')
9f95a23c 101 for section, user in users.items():
7c673cae
FG
102 _config_user(s3tests_conf, section, '{user}.{client}'.format(user=user, client=client))
103 log.debug('Creating user {user} on {host}'.format(user=s3tests_conf[section]['user_id'], host=client))
104 cluster_name, daemon_type, client_id = teuthology.split_role(client)
105 client_with_id = daemon_type + '.' + client_id
106 ctx.cluster.only(client).run(
107 args=[
108 'adjust-ulimits',
109 'ceph-coverage',
110 '{tdir}/archive/coverage'.format(tdir=testdir),
111 'radosgw-admin',
112 '-n', client_with_id,
113 'user', 'create',
114 '--uid', s3tests_conf[section]['user_id'],
115 '--display-name', s3tests_conf[section]['display_name'],
116 '--access-key', s3tests_conf[section]['access_key'],
117 '--secret', s3tests_conf[section]['secret_key'],
118 '--email', s3tests_conf[section]['email'],
9f95a23c 119 '--caps', 'user-policy=*',
7c673cae
FG
120 '--cluster', cluster_name,
121 ],
122 )
11fdf7f2
TL
123 ctx.cluster.only(client).run(
124 args=[
125 'adjust-ulimits',
126 'ceph-coverage',
127 '{tdir}/archive/coverage'.format(tdir=testdir),
128 'radosgw-admin',
129 '-n', client_with_id,
130 'mfa', 'create',
131 '--uid', s3tests_conf[section]['user_id'],
132 '--totp-serial', s3tests_conf[section]['totp_serial'],
133 '--totp-seed', s3tests_conf[section]['totp_seed'],
134 '--totp-seconds', s3tests_conf[section]['totp_seconds'],
135 '--totp-window', '8',
136 '--totp-seed-type', 'base32',
137 '--cluster', cluster_name,
138 ],
139 )
7c673cae
FG
140 try:
141 yield
142 finally:
143 for client in config['clients']:
144 for user in users.itervalues():
145 uid = '{user}.{client}'.format(user=user, client=client)
146 cluster_name, daemon_type, client_id = teuthology.split_role(client)
147 client_with_id = daemon_type + '.' + client_id
148 ctx.cluster.only(client).run(
149 args=[
150 'adjust-ulimits',
151 'ceph-coverage',
152 '{tdir}/archive/coverage'.format(tdir=testdir),
153 'radosgw-admin',
154 '-n', client_with_id,
155 'user', 'rm',
156 '--uid', uid,
157 '--purge-data',
158 '--cluster', cluster_name,
159 ],
160 )
161
162
163@contextlib.contextmanager
164def configure(ctx, config):
165 """
166 Configure the s3-tests. This includes the running of the
167 bootstrap code and the updating of local conf files.
168 """
169 assert isinstance(config, dict)
170 log.info('Configuring s3-tests...')
171 testdir = teuthology.get_testdir(ctx)
9f95a23c
TL
172 for client, properties in config['clients'].items():
173 properties = properties or {}
7c673cae 174 s3tests_conf = config['s3tests_conf'][client]
9f95a23c
TL
175 s3tests_conf['DEFAULT']['calling_format'] = properties.get('calling-format', 'ordinary')
176
177 # use rgw_server if given, or default to local client
178 role = properties.get('rgw_server', client)
179
180 endpoint = ctx.rgw.role_endpoints.get(role)
181 assert endpoint, 's3tests: no rgw endpoint for {}'.format(role)
182
183 s3tests_conf['DEFAULT']['host'] = endpoint.dns_name
184
185 website_role = properties.get('rgw_website_server')
186 if website_role:
187 website_endpoint = ctx.rgw.role_endpoints.get(website_role)
188 assert website_endpoint, \
189 's3tests: no rgw endpoint for rgw_website_server {}'.format(website_role)
190 assert website_endpoint.website_dns_name, \
191 's3tests: no dns-s3website-name for rgw_website_server {}'.format(website_role)
192 s3tests_conf['DEFAULT']['s3website_domain'] = website_endpoint.website_dns_name
193
194 if hasattr(ctx, 'barbican'):
195 properties = properties['barbican']
196 if properties is not None and 'kms_key' in properties:
197 if not (properties['kms_key'] in ctx.barbican.keys):
198 raise ConfigError('Key '+properties['kms_key']+' not defined')
199
200 if not (properties['kms_key2'] in ctx.barbican.keys):
201 raise ConfigError('Key '+properties['kms_key2']+' not defined')
202
203 key = ctx.barbican.keys[properties['kms_key']]
204 s3tests_conf['DEFAULT']['kms_keyid'] = key['id']
205
206 key = ctx.barbican.keys[properties['kms_key2']]
207 s3tests_conf['DEFAULT']['kms_keyid2'] = key['id']
208
209 elif hasattr(ctx, 'vault'):
210 properties = properties['vault_%s' % ctx.vault.engine]
211 s3tests_conf['DEFAULT']['kms_keyid'] = properties['key_path']
212 s3tests_conf['DEFAULT']['kms_keyid2'] = properties['key_path2']
213
7c673cae 214 else:
9f95a23c
TL
215 # Fallback scenario where it's the local (ceph.conf) kms being tested
216 s3tests_conf['DEFAULT']['kms_keyid'] = 'testkey-1'
217 s3tests_conf['DEFAULT']['kms_keyid2'] = 'testkey-2'
7c673cae 218
9f95a23c
TL
219 slow_backend = properties.get('slow_backend')
220 if slow_backend:
221 s3tests_conf['fixtures']['slow backend'] = slow_backend
7c673cae
FG
222
223 (remote,) = ctx.cluster.only(client).remotes.keys()
224 remote.run(
225 args=[
226 'cd',
227 '{tdir}/s3-tests'.format(tdir=testdir),
228 run.Raw('&&'),
229 './bootstrap',
230 ],
231 )
232 conf_fp = StringIO()
233 s3tests_conf.write(conf_fp)
234 teuthology.write_file(
235 remote=remote,
236 path='{tdir}/archive/s3-tests.{client}.conf'.format(tdir=testdir, client=client),
237 data=conf_fp.getvalue(),
238 )
239
240 log.info('Configuring boto...')
241 boto_src = os.path.join(os.path.dirname(__file__), 'boto.cfg.template')
9f95a23c
TL
242 for client, properties in config['clients'].items():
243 with open(boto_src, 'rb') as f:
7c673cae
FG
244 (remote,) = ctx.cluster.only(client).remotes.keys()
245 conf = f.read().format(
246 idle_timeout=config.get('idle_timeout', 30)
247 )
248 teuthology.write_file(
249 remote=remote,
250 path='{tdir}/boto.cfg'.format(tdir=testdir),
251 data=conf,
252 )
253
254 try:
255 yield
256
257 finally:
258 log.info('Cleaning up boto...')
9f95a23c 259 for client, properties in config['clients'].items():
7c673cae
FG
260 (remote,) = ctx.cluster.only(client).remotes.keys()
261 remote.run(
262 args=[
263 'rm',
264 '{tdir}/boto.cfg'.format(tdir=testdir),
265 ],
266 )
267
7c673cae
FG
268@contextlib.contextmanager
269def run_tests(ctx, config):
270 """
271 Run the s3tests after everything is set up.
272
273 :param ctx: Context passed to task
274 :param config: specific configuration information
275 """
276 assert isinstance(config, dict)
277 testdir = teuthology.get_testdir(ctx)
9f95a23c
TL
278 for client, client_config in config.items():
279 client_config = client_config or {}
11fdf7f2 280 (remote,) = ctx.cluster.only(client).remotes.keys()
7c673cae
FG
281 args = [
282 'S3TEST_CONF={tdir}/archive/s3-tests.{client}.conf'.format(tdir=testdir, client=client),
11fdf7f2
TL
283 'BOTO_CONFIG={tdir}/boto.cfg'.format(tdir=testdir)
284 ]
285 # the 'requests' library comes with its own ca bundle to verify ssl
286 # certificates - override that to use the system's ca bundle, which
287 # is where the ssl task installed this certificate
288 if remote.os.package_type == 'deb':
289 args += ['REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt']
290 else:
291 args += ['REQUESTS_CA_BUNDLE=/etc/pki/tls/certs/ca-bundle.crt']
9f95a23c
TL
292 # civetweb > 1.8 && beast parsers are strict on rfc2616
293 attrs = ["!fails_on_rgw", "!lifecycle_expiration", "!fails_strict_rfc2616"]
294 if client_config.get('calling-format') != 'ordinary':
295 attrs += ['!fails_with_subdomain']
11fdf7f2 296 args += [
9f95a23c
TL
297 '{tdir}/s3-tests/virtualenv/bin/python'.format(tdir=testdir),
298 '-m', 'nose',
7c673cae
FG
299 '-w',
300 '{tdir}/s3-tests'.format(tdir=testdir),
301 '-v',
302 '-a', ','.join(attrs),
303 ]
9f95a23c
TL
304 if 'extra_args' in client_config:
305 args.append(client_config['extra_args'])
7c673cae 306
11fdf7f2 307 remote.run(
7c673cae
FG
308 args=args,
309 label="s3 tests against rgw"
310 )
311 yield
312
313@contextlib.contextmanager
314def scan_for_leaked_encryption_keys(ctx, config):
315 """
316 Scan radosgw logs for the encryption keys used by s3tests to
317 verify that we're not leaking secrets.
318
319 :param ctx: Context passed to task
320 :param config: specific configuration information
321 """
322 assert isinstance(config, dict)
323
324 try:
325 yield
326 finally:
327 # x-amz-server-side-encryption-customer-key
328 s3test_customer_key = 'pO3upElrwuEXSoFwCfnZPdSsmt/xWeFa0N9KgDijwVs='
329
330 log.debug('Scanning radosgw logs for leaked encryption keys...')
331 procs = list()
9f95a23c 332 for client, client_config in config.items():
7c673cae
FG
333 if not client_config.get('scan_for_encryption_keys', True):
334 continue
335 cluster_name, daemon_type, client_id = teuthology.split_role(client)
336 client_with_cluster = '.'.join((cluster_name, daemon_type, client_id))
337 (remote,) = ctx.cluster.only(client).remotes.keys()
338 proc = remote.run(
339 args=[
340 'grep',
341 '--binary-files=text',
342 s3test_customer_key,
343 '/var/log/ceph/rgw.{client}.log'.format(client=client_with_cluster),
344 ],
345 wait=False,
346 check_status=False,
347 )
348 procs.append(proc)
349
350 for proc in procs:
351 proc.wait()
352 if proc.returncode == 1: # 1 means no matches
353 continue
354 log.error('radosgw log is leaking encryption keys!')
355 raise Exception('radosgw log is leaking encryption keys')
356
357@contextlib.contextmanager
358def task(ctx, config):
359 """
360 Run the s3-tests suite against rgw.
361
362 To run all tests on all clients::
363
364 tasks:
365 - ceph:
366 - rgw:
367 - s3tests:
368
369 To restrict testing to particular clients::
370
371 tasks:
372 - ceph:
373 - rgw: [client.0]
9f95a23c 374 - s3tests: [client.0]
7c673cae
FG
375
376 To run against a server on client.1 and increase the boto timeout to 10m::
377
378 tasks:
379 - ceph:
380 - rgw: [client.1]
381 - s3tests:
382 client.0:
383 rgw_server: client.1
384 idle_timeout: 600
385
386 To pass extra arguments to nose (e.g. to run a certain test)::
387
388 tasks:
389 - ceph:
390 - rgw: [client.0]
391 - s3tests:
392 client.0:
393 extra_args: ['test_s3:test_object_acl_grand_public_read']
394 client.1:
395 extra_args: ['--exclude', 'test_100_continue']
396 """
11fdf7f2 397 assert hasattr(ctx, 'rgw'), 's3tests must run after the rgw task'
7c673cae
FG
398 assert config is None or isinstance(config, list) \
399 or isinstance(config, dict), \
400 "task s3tests only supports a list or dictionary for configuration"
401 all_clients = ['client.{id}'.format(id=id_)
402 for id_ in teuthology.all_roles_of_type(ctx.cluster, 'client')]
403 if config is None:
404 config = all_clients
405 if isinstance(config, list):
406 config = dict.fromkeys(config)
407 clients = config.keys()
408
409 overrides = ctx.config.get('overrides', {})
410 # merge each client section, not the top level.
9f95a23c 411 for client in config.keys():
7c673cae
FG
412 if not config[client]:
413 config[client] = {}
414 teuthology.deep_merge(config[client], overrides.get('s3tests', {}))
415
416 log.debug('s3tests config is %s', config)
417
418 s3tests_conf = {}
419 for client in clients:
11fdf7f2
TL
420 endpoint = ctx.rgw.role_endpoints.get(client)
421 assert endpoint, 's3tests: no rgw endpoint for {}'.format(client)
422
7c673cae
FG
423 s3tests_conf[client] = ConfigObj(
424 indent_type='',
425 infile={
426 'DEFAULT':
427 {
11fdf7f2 428 'port' : endpoint.port,
494da23a 429 'is_secure' : endpoint.cert is not None,
11fdf7f2 430 'api_name' : 'default',
7c673cae
FG
431 },
432 'fixtures' : {},
433 's3 main' : {},
434 's3 alt' : {},
31f18b77 435 's3 tenant': {},
7c673cae
FG
436 }
437 )
438
7c673cae
FG
439 with contextutil.nested(
440 lambda: download(ctx=ctx, config=config),
441 lambda: create_users(ctx=ctx, config=dict(
442 clients=clients,
443 s3tests_conf=s3tests_conf,
444 )),
7c673cae
FG
445 lambda: configure(ctx=ctx, config=dict(
446 clients=config,
447 s3tests_conf=s3tests_conf,
448 )),
449 lambda: run_tests(ctx=ctx, config=config),
450 lambda: scan_for_leaked_encryption_keys(ctx=ctx, config=config),
451 ):
452 pass
453 yield