9 from cStringIO import StringIO
10 from teuthology.orchestra import run
11 from teuthology import misc as teuthology
12 from teuthology import contextutil
13 from teuthology.parallel import parallel
14 from teuthology.task.common_fs_utils import generic_mkfs
15 from teuthology.task.common_fs_utils import generic_mount
16 from teuthology.task.common_fs_utils import default_image_name
18 log = logging.getLogger(__name__)
20 @contextlib.contextmanager
21 def create_image(ctx, config):
36 Image size is expressed as a number of megabytes; default value
39 Image format value must be either 1 or 2; default value is 1.
42 assert isinstance(config, dict) or isinstance(config, list), \
43 "task create_image only supports a list or dictionary for configuration"
45 if isinstance(config, dict):
46 images = config.items()
48 images = [(role, None) for role in config]
50 testdir = teuthology.get_testdir(ctx)
51 for role, properties in images:
52 if properties is None:
54 name = properties.get('image_name', default_image_name(role))
55 size = properties.get('image_size', 10240)
56 fmt = properties.get('image_format', 1)
57 (remote,) = ctx.cluster.only(role).remotes.keys()
58 log.info('Creating image {name} with size {size}'.format(name=name,
62 'ceph-coverage'.format(tdir=testdir),
63 '{tdir}/archive/coverage'.format(tdir=testdir),
70 # omit format option if using the default (format 1)
71 # since old versions of don't support it
73 args += ['--image-format', str(fmt)]
78 log.info('Deleting rbd images...')
79 for role, properties in images:
80 if properties is None:
82 name = properties.get('image_name', default_image_name(role))
83 (remote,) = ctx.cluster.only(role).remotes.keys()
88 '{tdir}/archive/coverage'.format(tdir=testdir),
96 @contextlib.contextmanager
97 def clone_image(ctx, config):
107 parent_name: testimage
108 image_name: cloneimage
110 assert isinstance(config, dict) or isinstance(config, list), \
111 "task clone_image only supports a list or dictionary for configuration"
113 if isinstance(config, dict):
114 images = config.items()
116 images = [(role, None) for role in config]
118 testdir = teuthology.get_testdir(ctx)
119 for role, properties in images:
120 if properties is None:
123 name = properties.get('image_name', default_image_name(role))
124 parent_name = properties.get('parent_name')
125 assert parent_name is not None, \
126 "parent_name is required"
127 parent_spec = '{name}@{snap}'.format(name=parent_name, snap=name)
129 (remote,) = ctx.cluster.only(role).remotes.keys()
130 log.info('Clone image {parent} to {child}'.format(parent=parent_name,
132 for cmd in [('snap', 'create', parent_spec),
133 ('snap', 'protect', parent_spec),
134 ('clone', parent_spec, name)]:
137 'ceph-coverage'.format(tdir=testdir),
138 '{tdir}/archive/coverage'.format(tdir=testdir),
142 remote.run(args=args)
147 log.info('Deleting rbd clones...')
148 for role, properties in images:
149 if properties is None:
151 name = properties.get('image_name', default_image_name(role))
152 parent_name = properties.get('parent_name')
153 parent_spec = '{name}@{snap}'.format(name=parent_name, snap=name)
155 (remote,) = ctx.cluster.only(role).remotes.keys()
157 for cmd in [('rm', name),
158 ('snap', 'unprotect', parent_spec),
159 ('snap', 'rm', parent_spec)]:
162 'ceph-coverage'.format(tdir=testdir),
163 '{tdir}/archive/coverage'.format(tdir=testdir),
167 remote.run(args=args)
169 @contextlib.contextmanager
170 def modprobe(ctx, config):
172 Load the rbd kernel module..
178 - rbd.create_image: [client.0]
179 - rbd.modprobe: [client.0]
181 log.info('Loading rbd kernel module...')
183 (remote,) = ctx.cluster.only(role).remotes.keys()
194 log.info('Unloading rbd kernel module...')
196 (remote,) = ctx.cluster.only(role).remotes.keys()
203 # force errors to be ignored; necessary if more
204 # than one device was created, which may mean
205 # the module isn't quite ready to go the first
212 @contextlib.contextmanager
213 def dev_create(ctx, config):
215 Map block devices to rbd images.
221 - rbd.create_image: [client.0]
222 - rbd.modprobe: [client.0]
224 client.0: testimage.client.0
226 assert isinstance(config, dict) or isinstance(config, list), \
227 "task dev_create only supports a list or dictionary for configuration"
229 if isinstance(config, dict):
230 role_images = config.items()
232 role_images = [(role, None) for role in config]
234 log.info('Creating rbd block devices...')
236 testdir = teuthology.get_testdir(ctx)
238 for role, image in role_images:
240 image = default_image_name(role)
241 (remote,) = ctx.cluster.only(role).remotes.keys()
248 '{tdir}/archive/coverage'.format(tdir=testdir),
250 '--user', role.rsplit('.')[-1],
255 # wait for the symlink to be created by udev
256 'while', 'test', '!', '-e', '/dev/rbd/rbd/{image}'.format(image=image), run.Raw(';'), 'do',
257 'sleep', '1', run.Raw(';'),
264 log.info('Unmapping rbd devices...')
265 for role, image in role_images:
267 image = default_image_name(role)
268 (remote,) = ctx.cluster.only(role).remotes.keys()
271 'LD_LIBRARY_PATH={tdir}/binary/usr/local/lib'.format(tdir=testdir),
275 '{tdir}/archive/coverage'.format(tdir=testdir),
279 '/dev/rbd/rbd/{imgname}'.format(imgname=image),
281 # wait for the symlink to be deleted by udev
282 'while', 'test', '-e', '/dev/rbd/rbd/{image}'.format(image=image),
285 'sleep', '1', run.Raw(';'),
291 def rbd_devname_rtn(ctx, image):
292 return '/dev/rbd/rbd/{image}'.format(image=image)
294 def canonical_path(ctx, role, path):
296 Determine the canonical path for a given path on the host
297 representing the given role. A canonical path contains no
298 . or .. components, and includes no symbolic links.
300 version_fp = StringIO()
301 ctx.cluster.only(role).run(
302 args=[ 'readlink', '-f', path ],
305 canonical_path = version_fp.getvalue().rstrip('\n')
307 return canonical_path
309 @contextlib.contextmanager
310 def run_xfstests(ctx, config):
312 Run xfstests over specified devices.
314 Warning: both the test and scratch devices specified will be
315 overwritten. Normally xfstests modifies (but does not destroy)
316 the test device, but for now the run script used here re-makes
319 Note: Only one instance of xfstests can run on a single host at
320 a time, although this is not enforced.
322 This task in its current form needs some improvement. For
323 example, it assumes all roles provided in the config are
324 clients, and that the config provided is a list of key/value
325 pairs. For now please use the xfstests() interface, below.
335 scratch_dev: 'scratch_dev'
337 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
342 with parallel() as p:
343 for role, properties in config.items():
344 p.spawn(run_xfstests_one_client, ctx, role, properties)
347 def run_xfstests_one_client(ctx, role, properties):
349 Spawned routine to handle xfs tests for a single client
351 testdir = teuthology.get_testdir(ctx)
353 count = properties.get('count')
354 test_dev = properties.get('test_dev')
355 assert test_dev is not None, \
356 "task run_xfstests requires test_dev to be defined"
357 test_dev = canonical_path(ctx, role, test_dev)
359 scratch_dev = properties.get('scratch_dev')
360 assert scratch_dev is not None, \
361 "task run_xfstests requires scratch_dev to be defined"
362 scratch_dev = canonical_path(ctx, role, scratch_dev)
364 fs_type = properties.get('fs_type')
365 tests = properties.get('tests')
366 exclude_list = properties.get('exclude')
367 randomize = properties.get('randomize')
369 (remote,) = ctx.cluster.only(role).remotes.keys()
371 # Fetch the test script
372 test_root = teuthology.get_testdir(ctx)
373 test_script = 'run_xfstests.sh'
374 test_path = os.path.join(test_root, test_script)
376 xfstests_url = properties.get('xfstests_url')
377 assert xfstests_url is not None, \
378 "task run_xfstests requires xfstests_url to be defined"
380 xfstests_krbd_url = xfstests_url + '/' + test_script
382 log.info('Fetching {script} for {role} from {url}'.format(
385 url=xfstests_krbd_url))
387 args = [ 'wget', '-O', test_path, '--', xfstests_krbd_url ]
388 remote.run(args=args)
390 log.info('Running xfstests on {role}:'.format(role=role))
391 log.info(' iteration count: {count}:'.format(count=count))
392 log.info(' test device: {dev}'.format(dev=test_dev))
393 log.info(' scratch device: {dev}'.format(dev=scratch_dev))
394 log.info(' using fs_type: {fs_type}'.format(fs_type=fs_type))
395 log.info(' tests to run: {tests}'.format(tests=tests))
396 log.info(' exclude list: {}'.format(' '.join(exclude_list)))
397 log.info(' randomize: {randomize}'.format(randomize=randomize))
400 with tempfile.NamedTemporaryFile(bufsize=0, prefix='exclude') as exclude_file:
401 for test in exclude_list:
402 exclude_file.write("{}\n".format(test))
403 remote.put_file(exclude_file.name, exclude_file.name)
405 # Note that the device paths are interpreted using
406 # readlink -f <path> in order to get their canonical
407 # pathname (so it matches what the kernel remembers).
410 'TESTDIR={tdir}'.format(tdir=testdir),
413 '{tdir}/archive/coverage'.format(tdir=testdir),
422 args.extend(['-x', exclude_file.name])
426 args.extend(['--', tests])
427 remote.run(args=args, logger=log.getChild(role))
429 log.info('Removing {script} on {role}'.format(script=test_script,
431 remote.run(args=['rm', '-f', test_path])
433 @contextlib.contextmanager
434 def xfstests(ctx, config):
436 Run xfstests over rbd devices. This interface sets up all
437 required configuration automatically if not otherwise specified.
438 Note that only one instance of xfstests can run on a single host
439 at a time. By default, the set of tests specified is run once.
440 If a (non-zero) count value is supplied, the complete set of
441 tests will be run that number of times.
447 # Image sizes are in MB
451 test_image: 'test_image'
454 scratch_image: 'scratch_image'
458 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
462 xfstests_branch: master
463 xfstests_url: 'https://raw.github.com/ceph/branch/master/qa'
466 config = { 'all': None }
467 assert isinstance(config, dict) or isinstance(config, list), \
468 "task xfstests only supports a list or dictionary for configuration"
469 if isinstance(config, dict):
470 config = teuthology.replace_all_with_clients(ctx.cluster, config)
471 runs = config.items()
473 runs = [(role, None) for role in config]
475 running_xfstests = {}
476 for role, properties in runs:
477 assert role.startswith('client.'), \
478 "task xfstests can only run on client nodes"
479 for host, roles_for_host in ctx.cluster.remotes.items():
480 if role in roles_for_host:
481 assert host not in running_xfstests, \
482 "task xfstests allows only one instance at a time per host"
483 running_xfstests[host] = True
488 image_map_config = {}
489 scratch_map_config = {}
491 for role, properties in runs:
492 if properties is None:
495 test_image = properties.get('test_image', 'test_image.{role}'.format(role=role))
496 test_size = properties.get('test_size', 10000) # 10G
497 test_fmt = properties.get('test_format', 1)
498 scratch_image = properties.get('scratch_image', 'scratch_image.{role}'.format(role=role))
499 scratch_size = properties.get('scratch_size', 10000) # 10G
500 scratch_fmt = properties.get('scratch_format', 1)
502 images_config[role] = dict(
503 image_name=test_image,
504 image_size=test_size,
505 image_format=test_fmt,
508 scratch_config[role] = dict(
509 image_name=scratch_image,
510 image_size=scratch_size,
511 image_format=scratch_fmt,
514 xfstests_branch = properties.get('xfstests_branch', 'master')
515 xfstests_url = properties.get('xfstests_url', 'https://raw.github.com/ceph/ceph/{branch}/qa'.format(branch=xfstests_branch))
517 xfstests_config[role] = dict(
518 count=properties.get('count', 1),
519 test_dev='/dev/rbd/rbd/{image}'.format(image=test_image),
520 scratch_dev='/dev/rbd/rbd/{image}'.format(image=scratch_image),
521 fs_type=properties.get('fs_type', 'xfs'),
522 randomize=properties.get('randomize', False),
523 tests=properties.get('tests'),
524 exclude=properties.get('exclude', []),
525 xfstests_url=xfstests_url,
528 log.info('Setting up xfstests using RBD images:')
529 log.info(' test ({size} MB): {image}'.format(size=test_size,
531 log.info(' scratch ({size} MB): {image}'.format(size=scratch_size,
532 image=scratch_image))
533 modprobe_config[role] = None
534 image_map_config[role] = test_image
535 scratch_map_config[role] = scratch_image
537 with contextutil.nested(
538 lambda: create_image(ctx=ctx, config=images_config),
539 lambda: create_image(ctx=ctx, config=scratch_config),
540 lambda: modprobe(ctx=ctx, config=modprobe_config),
541 lambda: dev_create(ctx=ctx, config=image_map_config),
542 lambda: dev_create(ctx=ctx, config=scratch_map_config),
543 lambda: run_xfstests(ctx=ctx, config=xfstests_config),
548 @contextlib.contextmanager
549 def task(ctx, config):
551 Create and mount an rbd image.
553 For example, you can specify which clients to run on::
557 - rbd: [client.0, client.1]
559 There are a few image options::
564 client.0: # uses defaults
571 To use default options on all clients::
578 To create 20GiB images and format them with xfs on all clients::
588 config = { 'all': None }
590 if isinstance(config, dict):
591 norm_config = teuthology.replace_all_with_clients(ctx.cluster, config)
592 if isinstance(norm_config, dict):
594 for role, properties in norm_config.iteritems():
595 if properties is None:
597 role_images[role] = properties.get('image_name')
599 role_images = norm_config
601 log.debug('rbd config is: %s', norm_config)
603 with contextutil.nested(
604 lambda: create_image(ctx=ctx, config=norm_config),
605 lambda: modprobe(ctx=ctx, config=norm_config),
606 lambda: dev_create(ctx=ctx, config=role_images),
607 lambda: generic_mkfs(ctx=ctx, config=norm_config,
608 devname_rtn=rbd_devname_rtn),
609 lambda: generic_mount(ctx=ctx, config=role_images,
610 devname_rtn=rbd_devname_rtn),