Fix some bugs when testing opensds ansible
[stor4nfv.git] / src / ceph / qa / tasks / rbd.py
1 """
2 Rbd testing task
3 """
4 import contextlib
5 import logging
6 import os
7 import tempfile
8
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
17
18 log = logging.getLogger(__name__)
19
20 @contextlib.contextmanager
21 def create_image(ctx, config):
22     """
23     Create an rbd image.
24
25     For example::
26
27         tasks:
28         - ceph:
29         - rbd.create_image:
30             client.0:
31                 image_name: testimage
32                 image_size: 100
33                 image_format: 1
34             client.1:
35
36     Image size is expressed as a number of megabytes; default value
37     is 10240.
38
39     Image format value must be either 1 or 2; default value is 1.
40
41     """
42     assert isinstance(config, dict) or isinstance(config, list), \
43         "task create_image only supports a list or dictionary for configuration"
44
45     if isinstance(config, dict):
46         images = config.items()
47     else:
48         images = [(role, None) for role in config]
49
50     testdir = teuthology.get_testdir(ctx)
51     for role, properties in images:
52         if properties is None:
53             properties = {}
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,
59                                                                  size=size))
60         args = [
61                 'adjust-ulimits',
62                 'ceph-coverage'.format(tdir=testdir),
63                 '{tdir}/archive/coverage'.format(tdir=testdir),
64                 'rbd',
65                 '-p', 'rbd',
66                 'create',
67                 '--size', str(size),
68                 name,
69             ]
70         # omit format option if using the default (format 1)
71         # since old versions of don't support it
72         if int(fmt) != 1:
73             args += ['--image-format', str(fmt)]
74         remote.run(args=args)
75     try:
76         yield
77     finally:
78         log.info('Deleting rbd images...')
79         for role, properties in images:
80             if properties is None:
81                 properties = {}
82             name = properties.get('image_name', default_image_name(role))
83             (remote,) = ctx.cluster.only(role).remotes.keys()
84             remote.run(
85                 args=[
86                     'adjust-ulimits',
87                     'ceph-coverage',
88                     '{tdir}/archive/coverage'.format(tdir=testdir),
89                     'rbd',
90                     '-p', 'rbd',
91                     'rm',
92                     name,
93                     ],
94                 )
95
96 @contextlib.contextmanager
97 def clone_image(ctx, config):
98     """
99     Clones a parent imag
100
101     For example::
102
103         tasks:
104         - ceph:
105         - rbd.clone_image:
106             client.0:
107                 parent_name: testimage
108                 image_name: cloneimage
109     """
110     assert isinstance(config, dict) or isinstance(config, list), \
111         "task clone_image only supports a list or dictionary for configuration"
112
113     if isinstance(config, dict):
114         images = config.items()
115     else:
116         images = [(role, None) for role in config]
117
118     testdir = teuthology.get_testdir(ctx)
119     for role, properties in images:
120         if properties is None:
121             properties = {}
122
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)
128
129         (remote,) = ctx.cluster.only(role).remotes.keys()
130         log.info('Clone image {parent} to {child}'.format(parent=parent_name,
131                                                           child=name))
132         for cmd in [('snap', 'create', parent_spec),
133                     ('snap', 'protect', parent_spec),
134                     ('clone', parent_spec, name)]:
135             args = [
136                     'adjust-ulimits',
137                     'ceph-coverage'.format(tdir=testdir),
138                     '{tdir}/archive/coverage'.format(tdir=testdir),
139                     'rbd', '-p', 'rbd'
140                     ]
141             args.extend(cmd)
142             remote.run(args=args)
143
144     try:
145         yield
146     finally:
147         log.info('Deleting rbd clones...')
148         for role, properties in images:
149             if properties is None:
150                 properties = {}
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)
154
155             (remote,) = ctx.cluster.only(role).remotes.keys()
156
157             for cmd in [('rm', name),
158                         ('snap', 'unprotect', parent_spec),
159                         ('snap', 'rm', parent_spec)]:
160                 args = [
161                         'adjust-ulimits',
162                         'ceph-coverage'.format(tdir=testdir),
163                         '{tdir}/archive/coverage'.format(tdir=testdir),
164                         'rbd', '-p', 'rbd'
165                         ]
166                 args.extend(cmd)
167                 remote.run(args=args)
168
169 @contextlib.contextmanager
170 def modprobe(ctx, config):
171     """
172     Load the rbd kernel module..
173
174     For example::
175
176         tasks:
177         - ceph:
178         - rbd.create_image: [client.0]
179         - rbd.modprobe: [client.0]
180     """
181     log.info('Loading rbd kernel module...')
182     for role in config:
183         (remote,) = ctx.cluster.only(role).remotes.keys()
184         remote.run(
185             args=[
186                 'sudo',
187                 'modprobe',
188                 'rbd',
189                 ],
190             )
191     try:
192         yield
193     finally:
194         log.info('Unloading rbd kernel module...')
195         for role in config:
196             (remote,) = ctx.cluster.only(role).remotes.keys()
197             remote.run(
198                 args=[
199                     'sudo',
200                     'modprobe',
201                     '-r',
202                     'rbd',
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
206                     # time through.
207                     run.Raw('||'),
208                     'true',
209                     ],
210                 )
211
212 @contextlib.contextmanager
213 def dev_create(ctx, config):
214     """
215     Map block devices to rbd images.
216
217     For example::
218
219         tasks:
220         - ceph:
221         - rbd.create_image: [client.0]
222         - rbd.modprobe: [client.0]
223         - rbd.dev_create:
224             client.0: testimage.client.0
225     """
226     assert isinstance(config, dict) or isinstance(config, list), \
227         "task dev_create only supports a list or dictionary for configuration"
228
229     if isinstance(config, dict):
230         role_images = config.items()
231     else:
232         role_images = [(role, None) for role in config]
233
234     log.info('Creating rbd block devices...')
235
236     testdir = teuthology.get_testdir(ctx)
237
238     for role, image in role_images:
239         if image is None:
240             image = default_image_name(role)
241         (remote,) = ctx.cluster.only(role).remotes.keys()
242
243         remote.run(
244             args=[
245                 'sudo',
246                 'adjust-ulimits',
247                 'ceph-coverage',
248                 '{tdir}/archive/coverage'.format(tdir=testdir),
249                 'rbd',
250                 '--user', role.rsplit('.')[-1],
251                 '-p', 'rbd',
252                 'map',
253                 image,
254                 run.Raw('&&'),
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(';'),
258                 'done',
259                 ],
260             )
261     try:
262         yield
263     finally:
264         log.info('Unmapping rbd devices...')
265         for role, image in role_images:
266             if image is None:
267                 image = default_image_name(role)
268             (remote,) = ctx.cluster.only(role).remotes.keys()
269             remote.run(
270                 args=[
271                     'LD_LIBRARY_PATH={tdir}/binary/usr/local/lib'.format(tdir=testdir),
272                     'sudo',
273                     'adjust-ulimits',
274                     'ceph-coverage',
275                     '{tdir}/archive/coverage'.format(tdir=testdir),
276                     'rbd',
277                     '-p', 'rbd',
278                     'unmap',
279                     '/dev/rbd/rbd/{imgname}'.format(imgname=image),
280                     run.Raw('&&'),
281                     # wait for the symlink to be deleted by udev
282                     'while', 'test', '-e', '/dev/rbd/rbd/{image}'.format(image=image),
283                     run.Raw(';'),
284                     'do',
285                     'sleep', '1', run.Raw(';'),
286                     'done',
287                     ],
288                 )
289
290
291 def rbd_devname_rtn(ctx, image):
292     return '/dev/rbd/rbd/{image}'.format(image=image)    
293
294 def canonical_path(ctx, role, path):
295     """
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.
299     """
300     version_fp = StringIO()
301     ctx.cluster.only(role).run(
302         args=[ 'readlink', '-f', path ],
303         stdout=version_fp,
304         )
305     canonical_path = version_fp.getvalue().rstrip('\n')
306     version_fp.close()
307     return canonical_path
308
309 @contextlib.contextmanager
310 def run_xfstests(ctx, config):
311     """
312     Run xfstests over specified devices.
313
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
317     both filesystems.
318
319     Note: Only one instance of xfstests can run on a single host at
320     a time, although this is not enforced.
321
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.
326
327     For example::
328
329         tasks:
330         - ceph:
331         - rbd.run_xfstests:
332             client.0:
333                 count: 2
334                 test_dev: 'test_dev'
335                 scratch_dev: 'scratch_dev'
336                 fs_type: 'xfs'
337                 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
338                 exclude:
339                 - generic/42
340                 randomize: true
341     """
342     with parallel() as p:
343         for role, properties in config.items():
344             p.spawn(run_xfstests_one_client, ctx, role, properties)
345     yield
346
347 def run_xfstests_one_client(ctx, role, properties):
348     """
349     Spawned routine to handle xfs tests for a single client
350     """
351     testdir = teuthology.get_testdir(ctx)
352     try:
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)
358
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)
363
364         fs_type = properties.get('fs_type')
365         tests = properties.get('tests')
366         exclude_list = properties.get('exclude')
367         randomize = properties.get('randomize')
368
369         (remote,) = ctx.cluster.only(role).remotes.keys()
370
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)
375
376         xfstests_url = properties.get('xfstests_url')
377         assert xfstests_url is not None, \
378             "task run_xfstests requires xfstests_url to be defined"
379
380         xfstests_krbd_url = xfstests_url + '/' + test_script
381
382         log.info('Fetching {script} for {role} from {url}'.format(
383             script=test_script,
384             role=role,
385             url=xfstests_krbd_url))
386
387         args = [ 'wget', '-O', test_path, '--', xfstests_krbd_url ]
388         remote.run(args=args)
389
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))
398
399         if exclude_list:
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)
404
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).
408         args = [
409             '/usr/bin/sudo',
410             'TESTDIR={tdir}'.format(tdir=testdir),
411             'adjust-ulimits',
412             'ceph-coverage',
413             '{tdir}/archive/coverage'.format(tdir=testdir),
414             '/bin/bash',
415             test_path,
416             '-c', str(count),
417             '-f', fs_type,
418             '-t', test_dev,
419             '-s', scratch_dev,
420             ]
421         if exclude_list:
422             args.extend(['-x', exclude_file.name])
423         if randomize:
424             args.append('-r')
425         if tests:
426             args.extend(['--', tests])
427         remote.run(args=args, logger=log.getChild(role))
428     finally:
429         log.info('Removing {script} on {role}'.format(script=test_script,
430                                                       role=role))
431         remote.run(args=['rm', '-f', test_path])
432
433 @contextlib.contextmanager
434 def xfstests(ctx, config):
435     """
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.
442
443     For example::
444
445         tasks:
446         - ceph:
447         # Image sizes are in MB
448         - rbd.xfstests:
449             client.0:
450                 count: 3
451                 test_image: 'test_image'
452                 test_size: 250
453                 test_format: 2
454                 scratch_image: 'scratch_image'
455                 scratch_size: 250
456                 scratch_format: 1
457                 fs_type: 'xfs'
458                 tests: 'generic/100 xfs/003 xfs/005 xfs/006 generic/015'
459                 exclude:
460                 - generic/42
461                 randomize: true
462                 xfstests_branch: master
463                 xfstests_url: 'https://raw.github.com/ceph/branch/master/qa'
464     """
465     if config is None:
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()
472     else:
473         runs = [(role, None) for role in config]
474
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
484
485     images_config = {}
486     scratch_config = {}
487     modprobe_config = {}
488     image_map_config = {}
489     scratch_map_config = {}
490     xfstests_config = {}
491     for role, properties in runs:
492         if properties is None:
493             properties = {}
494
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)
501
502         images_config[role] = dict(
503             image_name=test_image,
504             image_size=test_size,
505             image_format=test_fmt,
506             )
507
508         scratch_config[role] = dict(
509             image_name=scratch_image,
510             image_size=scratch_size,
511             image_format=scratch_fmt,
512             )
513
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))
516
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,
526             )
527
528         log.info('Setting up xfstests using RBD images:')
529         log.info('      test ({size} MB): {image}'.format(size=test_size,
530                                                         image=test_image))
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
536
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),
544         ):
545         yield
546
547
548 @contextlib.contextmanager
549 def task(ctx, config):
550     """
551     Create and mount an rbd image.
552
553     For example, you can specify which clients to run on::
554
555         tasks:
556         - ceph:
557         - rbd: [client.0, client.1]
558
559     There are a few image options::
560
561         tasks:
562         - ceph:
563         - rbd:
564             client.0: # uses defaults
565             client.1:
566                 image_name: foo
567                 image_size: 2048
568                 image_format: 2
569                 fs_type: xfs
570
571     To use default options on all clients::
572
573         tasks:
574         - ceph:
575         - rbd:
576             all:
577
578     To create 20GiB images and format them with xfs on all clients::
579
580         tasks:
581         - ceph:
582         - rbd:
583             all:
584               image_size: 20480
585               fs_type: xfs
586     """
587     if config is None:
588         config = { 'all': None }
589     norm_config = config
590     if isinstance(config, dict):
591         norm_config = teuthology.replace_all_with_clients(ctx.cluster, config)
592     if isinstance(norm_config, dict):
593         role_images = {}
594         for role, properties in norm_config.iteritems():
595             if properties is None:
596                 properties = {}
597             role_images[role] = properties.get('image_name')
598     else:
599         role_images = norm_config
600
601     log.debug('rbd config is: %s', norm_config)
602
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),
611         ):
612         yield