]> git.proxmox.com Git - ceph.git/blame - ceph/qa/tasks/rbd.py
import 15.2.0 Octopus source
[ceph.git] / ceph / qa / tasks / rbd.py
CommitLineData
7c673cae
FG
1"""
2Rbd testing task
3"""
4import contextlib
5import logging
6import os
181888fb 7import tempfile
11fdf7f2 8import sys
7c673cae
FG
9
10from cStringIO import StringIO
11from teuthology.orchestra import run
12from teuthology import misc as teuthology
13from teuthology import contextutil
14from teuthology.parallel import parallel
15from teuthology.task.common_fs_utils import generic_mkfs
16from teuthology.task.common_fs_utils import generic_mount
17from teuthology.task.common_fs_utils import default_image_name
18
9f95a23c
TL
19import six
20
11fdf7f2
TL
21#V1 image unsupported but required for testing purposes
22os.environ["RBD_FORCE_ALLOW_V1"] = "1"
23
7c673cae
FG
24log = logging.getLogger(__name__)
25
26@contextlib.contextmanager
27def create_image(ctx, config):
28 """
29 Create an rbd image.
30
31 For example::
32
33 tasks:
34 - ceph:
35 - rbd.create_image:
36 client.0:
37 image_name: testimage
38 image_size: 100
39 image_format: 1
40 client.1:
41
42 Image size is expressed as a number of megabytes; default value
43 is 10240.
44
45 Image format value must be either 1 or 2; default value is 1.
46
47 """
48 assert isinstance(config, dict) or isinstance(config, list), \
49 "task create_image only supports a list or dictionary for configuration"
50
51 if isinstance(config, dict):
52 images = config.items()
53 else:
54 images = [(role, None) for role in config]
55
56 testdir = teuthology.get_testdir(ctx)
57 for role, properties in images:
58 if properties is None:
59 properties = {}
60 name = properties.get('image_name', default_image_name(role))
61 size = properties.get('image_size', 10240)
62 fmt = properties.get('image_format', 1)
63 (remote,) = ctx.cluster.only(role).remotes.keys()
64 log.info('Creating image {name} with size {size}'.format(name=name,
65 size=size))
66 args = [
67 'adjust-ulimits',
9f95a23c 68 'ceph-coverage',
7c673cae
FG
69 '{tdir}/archive/coverage'.format(tdir=testdir),
70 'rbd',
71 '-p', 'rbd',
72 'create',
73 '--size', str(size),
74 name,
75 ]
76 # omit format option if using the default (format 1)
77 # since old versions of don't support it
78 if int(fmt) != 1:
79 args += ['--image-format', str(fmt)]
80 remote.run(args=args)
81 try:
82 yield
83 finally:
84 log.info('Deleting rbd images...')
85 for role, properties in images:
86 if properties is None:
87 properties = {}
88 name = properties.get('image_name', default_image_name(role))
89 (remote,) = ctx.cluster.only(role).remotes.keys()
90 remote.run(
91 args=[
92 'adjust-ulimits',
93 'ceph-coverage',
94 '{tdir}/archive/coverage'.format(tdir=testdir),
95 'rbd',
96 '-p', 'rbd',
97 'rm',
98 name,
99 ],
100 )
101
102@contextlib.contextmanager
103def clone_image(ctx, config):
104 """
105 Clones a parent imag
106
107 For example::
108
109 tasks:
110 - ceph:
111 - rbd.clone_image:
112 client.0:
113 parent_name: testimage
114 image_name: cloneimage
115 """
116 assert isinstance(config, dict) or isinstance(config, list), \
117 "task clone_image only supports a list or dictionary for configuration"
118
119 if isinstance(config, dict):
120 images = config.items()
121 else:
122 images = [(role, None) for role in config]
123
124 testdir = teuthology.get_testdir(ctx)
125 for role, properties in images:
126 if properties is None:
127 properties = {}
128
129 name = properties.get('image_name', default_image_name(role))
130 parent_name = properties.get('parent_name')
131 assert parent_name is not None, \
132 "parent_name is required"
133 parent_spec = '{name}@{snap}'.format(name=parent_name, snap=name)
134
135 (remote,) = ctx.cluster.only(role).remotes.keys()
136 log.info('Clone image {parent} to {child}'.format(parent=parent_name,
137 child=name))
138 for cmd in [('snap', 'create', parent_spec),
139 ('snap', 'protect', parent_spec),
140 ('clone', parent_spec, name)]:
141 args = [
142 'adjust-ulimits',
9f95a23c 143 'ceph-coverage',
7c673cae
FG
144 '{tdir}/archive/coverage'.format(tdir=testdir),
145 'rbd', '-p', 'rbd'
146 ]
147 args.extend(cmd)
148 remote.run(args=args)
149
150 try:
151 yield
152 finally:
153 log.info('Deleting rbd clones...')
154 for role, properties in images:
155 if properties is None:
156 properties = {}
157 name = properties.get('image_name', default_image_name(role))
158 parent_name = properties.get('parent_name')
159 parent_spec = '{name}@{snap}'.format(name=parent_name, snap=name)
160
161 (remote,) = ctx.cluster.only(role).remotes.keys()
162
163 for cmd in [('rm', name),
164 ('snap', 'unprotect', parent_spec),
165 ('snap', 'rm', parent_spec)]:
166 args = [
167 'adjust-ulimits',
9f95a23c 168 'ceph-coverage',
7c673cae
FG
169 '{tdir}/archive/coverage'.format(tdir=testdir),
170 'rbd', '-p', 'rbd'
171 ]
172 args.extend(cmd)
173 remote.run(args=args)
174
175@contextlib.contextmanager
176def modprobe(ctx, config):
177 """
178 Load the rbd kernel module..
179
180 For example::
181
182 tasks:
183 - ceph:
184 - rbd.create_image: [client.0]
185 - rbd.modprobe: [client.0]
186 """
187 log.info('Loading rbd kernel module...')
188 for role in config:
189 (remote,) = ctx.cluster.only(role).remotes.keys()
190 remote.run(
191 args=[
192 'sudo',
193 'modprobe',
194 'rbd',
195 ],
196 )
197 try:
198 yield
199 finally:
200 log.info('Unloading rbd kernel module...')
201 for role in config:
202 (remote,) = ctx.cluster.only(role).remotes.keys()
203 remote.run(
204 args=[
205 'sudo',
206 'modprobe',
207 '-r',
208 'rbd',
209 # force errors to be ignored; necessary if more
210 # than one device was created, which may mean
211 # the module isn't quite ready to go the first
212 # time through.
213 run.Raw('||'),
214 'true',
215 ],
216 )
217
218@contextlib.contextmanager
219def dev_create(ctx, config):
220 """
221 Map block devices to rbd images.
222
223 For example::
224
225 tasks:
226 - ceph:
227 - rbd.create_image: [client.0]
228 - rbd.modprobe: [client.0]
229 - rbd.dev_create:
230 client.0: testimage.client.0
231 """
232 assert isinstance(config, dict) or isinstance(config, list), \
233 "task dev_create only supports a list or dictionary for configuration"
234
235 if isinstance(config, dict):
236 role_images = config.items()
237 else:
238 role_images = [(role, None) for role in config]
239
240 log.info('Creating rbd block devices...')
241
242 testdir = teuthology.get_testdir(ctx)
243
244 for role, image in role_images:
245 if image is None:
246 image = default_image_name(role)
247 (remote,) = ctx.cluster.only(role).remotes.keys()
248
249 remote.run(
250 args=[
251 'sudo',
252 'adjust-ulimits',
253 'ceph-coverage',
254 '{tdir}/archive/coverage'.format(tdir=testdir),
255 'rbd',
256 '--user', role.rsplit('.')[-1],
257 '-p', 'rbd',
258 'map',
259 image,
260 run.Raw('&&'),
261 # wait for the symlink to be created by udev
262 'while', 'test', '!', '-e', '/dev/rbd/rbd/{image}'.format(image=image), run.Raw(';'), 'do',
263 'sleep', '1', run.Raw(';'),
264 'done',
265 ],
266 )
267 try:
268 yield
269 finally:
270 log.info('Unmapping rbd devices...')
271 for role, image in role_images:
272 if image is None:
273 image = default_image_name(role)
274 (remote,) = ctx.cluster.only(role).remotes.keys()
275 remote.run(
276 args=[
277 'LD_LIBRARY_PATH={tdir}/binary/usr/local/lib'.format(tdir=testdir),
278 'sudo',
279 'adjust-ulimits',
280 'ceph-coverage',
281 '{tdir}/archive/coverage'.format(tdir=testdir),
282 'rbd',
283 '-p', 'rbd',
284 'unmap',
285 '/dev/rbd/rbd/{imgname}'.format(imgname=image),
286 run.Raw('&&'),
287 # wait for the symlink to be deleted by udev
288 'while', 'test', '-e', '/dev/rbd/rbd/{image}'.format(image=image),
289 run.Raw(';'),
290 'do',
291 'sleep', '1', run.Raw(';'),
292 'done',
293 ],
294 )
295
296
297def rbd_devname_rtn(ctx, image):
298 return '/dev/rbd/rbd/{image}'.format(image=image)
299
300def canonical_path(ctx, role, path):
301 """
302 Determine the canonical path for a given path on the host
303 representing the given role. A canonical path contains no
304 . or .. components, and includes no symbolic links.
305 """
306 version_fp = StringIO()
307 ctx.cluster.only(role).run(
308 args=[ 'readlink', '-f', path ],
309 stdout=version_fp,
310 )
311 canonical_path = version_fp.getvalue().rstrip('\n')
312 version_fp.close()
313 return canonical_path
314
315@contextlib.contextmanager
316def run_xfstests(ctx, config):
317 """
318 Run xfstests over specified devices.
319
320 Warning: both the test and scratch devices specified will be
321 overwritten. Normally xfstests modifies (but does not destroy)
322 the test device, but for now the run script used here re-makes
323 both filesystems.
324
325 Note: Only one instance of xfstests can run on a single host at
326 a time, although this is not enforced.
327
328 This task in its current form needs some improvement. For
329 example, it assumes all roles provided in the config are
330 clients, and that the config provided is a list of key/value
331 pairs. For now please use the xfstests() interface, below.
332
333 For example::
334
335 tasks:
336 - ceph:
337 - rbd.run_xfstests:
338 client.0:
339 count: 2
340 test_dev: 'test_dev'
341 scratch_dev: 'scratch_dev'
342 fs_type: 'xfs'
343 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
181888fb
FG
344 exclude:
345 - generic/42
7c673cae
FG
346 randomize: true
347 """
348 with parallel() as p:
349 for role, properties in config.items():
350 p.spawn(run_xfstests_one_client, ctx, role, properties)
11fdf7f2
TL
351 exc_info = None
352 while True:
353 try:
354 p.next()
355 except StopIteration:
356 break
357 except:
358 exc_info = sys.exc_info()
359 if exc_info:
9f95a23c 360 six.reraise(exc_info[0], exc_info[1], exc_info[2])
7c673cae
FG
361 yield
362
363def run_xfstests_one_client(ctx, role, properties):
364 """
365 Spawned routine to handle xfs tests for a single client
366 """
367 testdir = teuthology.get_testdir(ctx)
368 try:
369 count = properties.get('count')
370 test_dev = properties.get('test_dev')
371 assert test_dev is not None, \
372 "task run_xfstests requires test_dev to be defined"
373 test_dev = canonical_path(ctx, role, test_dev)
374
375 scratch_dev = properties.get('scratch_dev')
376 assert scratch_dev is not None, \
377 "task run_xfstests requires scratch_dev to be defined"
378 scratch_dev = canonical_path(ctx, role, scratch_dev)
379
380 fs_type = properties.get('fs_type')
381 tests = properties.get('tests')
181888fb 382 exclude_list = properties.get('exclude')
7c673cae
FG
383 randomize = properties.get('randomize')
384
7c673cae
FG
385 (remote,) = ctx.cluster.only(role).remotes.keys()
386
387 # Fetch the test script
388 test_root = teuthology.get_testdir(ctx)
181888fb 389 test_script = 'run_xfstests.sh'
7c673cae
FG
390 test_path = os.path.join(test_root, test_script)
391
392 xfstests_url = properties.get('xfstests_url')
393 assert xfstests_url is not None, \
394 "task run_xfstests requires xfstests_url to be defined"
395
396 xfstests_krbd_url = xfstests_url + '/' + test_script
397
398 log.info('Fetching {script} for {role} from {url}'.format(
399 script=test_script,
400 role=role,
401 url=xfstests_krbd_url))
402
403 args = [ 'wget', '-O', test_path, '--', xfstests_krbd_url ]
404 remote.run(args=args)
405
406 log.info('Running xfstests on {role}:'.format(role=role))
407 log.info(' iteration count: {count}:'.format(count=count))
408 log.info(' test device: {dev}'.format(dev=test_dev))
409 log.info(' scratch device: {dev}'.format(dev=scratch_dev))
410 log.info(' using fs_type: {fs_type}'.format(fs_type=fs_type))
411 log.info(' tests to run: {tests}'.format(tests=tests))
181888fb 412 log.info(' exclude list: {}'.format(' '.join(exclude_list)))
7c673cae
FG
413 log.info(' randomize: {randomize}'.format(randomize=randomize))
414
181888fb
FG
415 if exclude_list:
416 with tempfile.NamedTemporaryFile(bufsize=0, prefix='exclude') as exclude_file:
417 for test in exclude_list:
418 exclude_file.write("{}\n".format(test))
419 remote.put_file(exclude_file.name, exclude_file.name)
420
7c673cae
FG
421 # Note that the device paths are interpreted using
422 # readlink -f <path> in order to get their canonical
423 # pathname (so it matches what the kernel remembers).
424 args = [
425 '/usr/bin/sudo',
426 'TESTDIR={tdir}'.format(tdir=testdir),
7c673cae
FG
427 'adjust-ulimits',
428 'ceph-coverage',
429 '{tdir}/archive/coverage'.format(tdir=testdir),
430 '/bin/bash',
431 test_path,
432 '-c', str(count),
433 '-f', fs_type,
434 '-t', test_dev,
435 '-s', scratch_dev,
436 ]
181888fb
FG
437 if exclude_list:
438 args.extend(['-x', exclude_file.name])
7c673cae
FG
439 if randomize:
440 args.append('-r')
441 if tests:
442 args.extend(['--', tests])
443 remote.run(args=args, logger=log.getChild(role))
444 finally:
445 log.info('Removing {script} on {role}'.format(script=test_script,
446 role=role))
447 remote.run(args=['rm', '-f', test_path])
448
449@contextlib.contextmanager
450def xfstests(ctx, config):
451 """
452 Run xfstests over rbd devices. This interface sets up all
453 required configuration automatically if not otherwise specified.
454 Note that only one instance of xfstests can run on a single host
455 at a time. By default, the set of tests specified is run once.
456 If a (non-zero) count value is supplied, the complete set of
457 tests will be run that number of times.
458
459 For example::
460
461 tasks:
462 - ceph:
463 # Image sizes are in MB
464 - rbd.xfstests:
465 client.0:
466 count: 3
467 test_image: 'test_image'
468 test_size: 250
469 test_format: 2
470 scratch_image: 'scratch_image'
471 scratch_size: 250
472 scratch_format: 1
473 fs_type: 'xfs'
474 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
181888fb
FG
475 exclude:
476 - generic/42
7c673cae
FG
477 randomize: true
478 xfstests_branch: master
479 xfstests_url: 'https://raw.github.com/ceph/branch/master/qa'
480 """
481 if config is None:
482 config = { 'all': None }
483 assert isinstance(config, dict) or isinstance(config, list), \
484 "task xfstests only supports a list or dictionary for configuration"
485 if isinstance(config, dict):
486 config = teuthology.replace_all_with_clients(ctx.cluster, config)
487 runs = config.items()
488 else:
489 runs = [(role, None) for role in config]
490
491 running_xfstests = {}
492 for role, properties in runs:
493 assert role.startswith('client.'), \
494 "task xfstests can only run on client nodes"
495 for host, roles_for_host in ctx.cluster.remotes.items():
496 if role in roles_for_host:
497 assert host not in running_xfstests, \
498 "task xfstests allows only one instance at a time per host"
499 running_xfstests[host] = True
500
501 images_config = {}
502 scratch_config = {}
503 modprobe_config = {}
504 image_map_config = {}
505 scratch_map_config = {}
506 xfstests_config = {}
507 for role, properties in runs:
508 if properties is None:
509 properties = {}
510
511 test_image = properties.get('test_image', 'test_image.{role}'.format(role=role))
512 test_size = properties.get('test_size', 10000) # 10G
513 test_fmt = properties.get('test_format', 1)
514 scratch_image = properties.get('scratch_image', 'scratch_image.{role}'.format(role=role))
515 scratch_size = properties.get('scratch_size', 10000) # 10G
516 scratch_fmt = properties.get('scratch_format', 1)
517
518 images_config[role] = dict(
519 image_name=test_image,
520 image_size=test_size,
521 image_format=test_fmt,
522 )
523
524 scratch_config[role] = dict(
525 image_name=scratch_image,
526 image_size=scratch_size,
527 image_format=scratch_fmt,
528 )
529
530 xfstests_branch = properties.get('xfstests_branch', 'master')
531 xfstests_url = properties.get('xfstests_url', 'https://raw.github.com/ceph/ceph/{branch}/qa'.format(branch=xfstests_branch))
532
533 xfstests_config[role] = dict(
534 count=properties.get('count', 1),
535 test_dev='/dev/rbd/rbd/{image}'.format(image=test_image),
536 scratch_dev='/dev/rbd/rbd/{image}'.format(image=scratch_image),
537 fs_type=properties.get('fs_type', 'xfs'),
538 randomize=properties.get('randomize', False),
539 tests=properties.get('tests'),
181888fb 540 exclude=properties.get('exclude', []),
7c673cae
FG
541 xfstests_url=xfstests_url,
542 )
543
544 log.info('Setting up xfstests using RBD images:')
545 log.info(' test ({size} MB): {image}'.format(size=test_size,
546 image=test_image))
547 log.info(' scratch ({size} MB): {image}'.format(size=scratch_size,
548 image=scratch_image))
549 modprobe_config[role] = None
550 image_map_config[role] = test_image
551 scratch_map_config[role] = scratch_image
552
553 with contextutil.nested(
554 lambda: create_image(ctx=ctx, config=images_config),
555 lambda: create_image(ctx=ctx, config=scratch_config),
556 lambda: modprobe(ctx=ctx, config=modprobe_config),
557 lambda: dev_create(ctx=ctx, config=image_map_config),
558 lambda: dev_create(ctx=ctx, config=scratch_map_config),
559 lambda: run_xfstests(ctx=ctx, config=xfstests_config),
560 ):
561 yield
562
563
564@contextlib.contextmanager
565def task(ctx, config):
566 """
567 Create and mount an rbd image.
568
569 For example, you can specify which clients to run on::
570
571 tasks:
572 - ceph:
573 - rbd: [client.0, client.1]
574
575 There are a few image options::
576
577 tasks:
578 - ceph:
579 - rbd:
580 client.0: # uses defaults
581 client.1:
582 image_name: foo
583 image_size: 2048
584 image_format: 2
585 fs_type: xfs
586
587 To use default options on all clients::
588
589 tasks:
590 - ceph:
591 - rbd:
592 all:
593
594 To create 20GiB images and format them with xfs on all clients::
595
596 tasks:
597 - ceph:
598 - rbd:
599 all:
600 image_size: 20480
601 fs_type: xfs
602 """
603 if config is None:
604 config = { 'all': None }
605 norm_config = config
606 if isinstance(config, dict):
607 norm_config = teuthology.replace_all_with_clients(ctx.cluster, config)
608 if isinstance(norm_config, dict):
609 role_images = {}
9f95a23c 610 for role, properties in norm_config.items():
7c673cae
FG
611 if properties is None:
612 properties = {}
613 role_images[role] = properties.get('image_name')
614 else:
615 role_images = norm_config
616
617 log.debug('rbd config is: %s', norm_config)
618
619 with contextutil.nested(
620 lambda: create_image(ctx=ctx, config=norm_config),
621 lambda: modprobe(ctx=ctx, config=norm_config),
622 lambda: dev_create(ctx=ctx, config=role_images),
623 lambda: generic_mkfs(ctx=ctx, config=norm_config,
624 devname_rtn=rbd_devname_rtn),
625 lambda: generic_mount(ctx=ctx, config=role_images,
626 devname_rtn=rbd_devname_rtn),
627 ):
628 yield