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