rbd.py: add xfstests functionality

Add tasks for running xfstests over a pair of rbd volumes.  The main
one is called xfstests, and it sets up rbd volumes of specified size
and runs a set of likely-to-be-successful tests.  The other one is
used by the first, and is called run_xfstests.  This provides a
generic (device rather than rbd device oriented) interface to
xfstests, and should probably be made standalone and distinct from
rbd at some point.

Using multiple rbd devices required the rbd udev rule manipulation
to ignore errors, since it appears that each device caused the a
teardown attempt, which leads to failures the second time around.
There's probably a more robust solution, but this works for now.

Signed-off-by: Alex Elder <elder@dreamhost.com>
This commit is contained in:
Sage Weil 2012-04-13 22:28:05 -07:00
parent ddb98f7773
commit 6ba4efcd3a

View File

@ -1,6 +1,8 @@
import contextlib
import logging
import os
from cStringIO import StringIO
from ..orchestra import run
from teuthology import misc as teuthology
from teuthology import contextutil
@ -112,6 +114,12 @@ def modprobe(ctx, config):
'modprobe',
'-r',
'rbd',
# force errors to be ignored; necessary if more
# than one device was created, which may mean
# the module isn't quite ready to go the first
# time through.
run.Raw('||'),
'true',
],
)
@ -218,6 +226,7 @@ def dev_create(ctx, config):
args=[
'sudo',
'rm',
'-f',
'/etc/udev/rules.d/51-rbd.rules',
],
wait=False,
@ -340,6 +349,201 @@ def mount(ctx, config):
]
)
# Determine the canonical path for a given path on the host
# representing the given role. A canonical path contains no
# . or .. components, and includes no symbolic links.
def canonical_path(ctx, role, path):
version_fp = StringIO()
ctx.cluster.only(role).run(
args=[ 'readlink', '-f', path ],
stdout=version_fp,
)
canonical_path = version_fp.getvalue().rstrip('\n')
version_fp.close()
return canonical_path
@contextlib.contextmanager
def run_xfstests(ctx, config):
"""
Run xfstests over specified devices.
Warning: both the test and scratch devices specified will be
overwritten. Normally xfstests modifies (but does not destroy)
the test device, but for now the run script used here re-makes
both filesystems.
Note: Only one instance of xfstests can run on a single host at
a time, although this is not enforced.
This task in its current form needs some improvement. For
example, it assumes all roles provided in the config are
clients, and that the config provided is a list of key/value
pairs. For now please use the xfstests() interface, below.
For example::
tasks:
- ceph:
- rbd.run_xfstests:
client.0:
test_dev: 'test_dev'
scratch_dev: 'scratch_dev'
fs_type: 'xfs'
tests: '1-9 11-15 17 19-21 26-28 31-34 41 45-48'
"""
for role, properties in config.items():
test_dev = properties.get('test_dev')
assert test_dev is not None, \
"task run_xfstests requires test_dev to be defined"
test_dev = canonical_path(ctx, role, test_dev)
scratch_dev = properties.get('scratch_dev')
assert scratch_dev is not None, \
"task run_xfstests requires scratch_dev to be defined"
scratch_dev = canonical_path(ctx, role, scratch_dev)
fs_type = properties.get('fs_type')
tests = properties.get('tests')
(remote,) = ctx.cluster.only(role).remotes.keys()
# Fetch the test script
test_root = '/tmp/cephtest'
test_script = 'run_xfstests.sh'
test_path = os.path.join(test_root, test_script)
git_branch = 'master'
test_url = 'https://raw.github.com/ceph/ceph/{branch}/qa/{script}'.format(branch=git_branch, script=test_script)
# test_url = 'http://ceph.newdream.net/git/?p=ceph.git;a=blob_plain;hb=refs/heads/{branch};f=qa/{script}'.format(branch=git_branch, script=test_script)
log.info('Fetching {script} for {role} from {url}'.format(script=test_script,
role=role,
url=test_url))
args = [ 'wget', '-O', test_path, '--', test_url ]
remote.run(args=args)
log.info('Running xfstests on {role}:'.format(role=role))
log.info(' test device: {dev}'.format(dev=test_dev))
log.info(' scratch device: {dev}'.format(dev=scratch_dev))
log.info(' using fs_type: {fs_type}'.format(fs_type=fs_type))
log.info(' tests to run: {tests}'.format(tests=tests))
# Note that the device paths are interpreted using
# readlink -f <path> in order to get their canonical
# pathname (so it matches what the kernel remembers).
args = [
'LD_LIBRARY_PATH=/tmp/cephtest/binary/usr/local/lib',
'/tmp/cephtest/enable-coredump',
'/tmp/cephtest/binary/usr/local/bin/ceph-coverage',
'/tmp/cephtest/archive/coverage',
'/usr/bin/sudo',
'/bin/bash',
test_path,
'-f', fs_type,
'-t', test_dev,
'-s', scratch_dev,
]
if tests:
args.append(tests)
remote.run(args=args)
try:
yield
finally:
for role, properties in config.items():
(remote,) = ctx.cluster.only(role).remotes.keys()
log.info('Removing {script} on {role}'.format(script=test_script,
role=role))
args = [ 'rm', '-f', test_path ]
remote.run(args=args)
@contextlib.contextmanager
def xfstests(ctx, config):
"""
Run xfstests over rbd devices. This interface sets up all
required configuration automatically if not otherwise specified.
Note that only one instance of xfstests can run on a single host
at a time.
For example::
tasks:
- ceph:
# Image sizes are in MB
- rbd.xfstests:
client.0:
test_image: 'test_image'
test_size: 100
scratch_image: 'scratch_image'
scratch_size: 250
fs_type: 'xfs'
tests: '1-9 11-15 17 19-21 26-28 31-34 41 45-48'
"""
if config is None:
config = { 'all': None }
assert isinstance(config, dict) or isinstance(config, list), \
"task xfstests only supports a list or dictionary for configuration"
if isinstance(config, dict):
config = teuthology.replace_all_with_clients(ctx.cluster, config)
runs = config.items()
else:
runs = [(role, None) for role in config]
running_xfstests = {}
for role, properties in runs:
assert role.startswith('client.'), \
"task xfstests can only run on client nodes"
for host, roles_for_host in ctx.cluster.remotes.items():
if role in roles_for_host:
assert host not in running_xfstests, \
"task xfstests allows only one instance at a time per host"
running_xfstests[host] = True
for role, properties in runs:
if properties is None:
properties = {}
test_image = properties.get('test_image', 'test_image')
test_size = properties.get('test_size', 100)
scratch_image = properties.get('scratch_image', 'scratch_image')
scratch_size = properties.get('scratch_size', 250)
test_image_config = {}
test_image_config['image_name'] = test_image
test_image_config['image_size'] = test_size
scratch_image_config = {}
scratch_image_config['image_name'] = scratch_image
scratch_image_config['image_size'] = scratch_size
test_config = {}
test_config['test_dev'] = \
'/dev/rbd/rbd/{image}'.format(image=test_image)
test_config['scratch_dev'] = \
'/dev/rbd/rbd/{image}'.format(image=scratch_image)
test_config['fs_type'] = properties.get('fs_type', 'xfs')
test_config['tests'] = properties.get('tests', None)
log.info('Setting up xfstests using RBD images:')
log.info(' test ({size} MB): {image}'.format(size=test_size,
image=test_image))
log.info(' scratch ({size} MB): {image}'.format(size=scratch_size,
image=scratch_image))
with contextutil.nested(
lambda: create_image(ctx=ctx, \
config={ role: test_image_config }),
lambda: create_image(ctx=ctx, \
config={ role: scratch_image_config }),
lambda: modprobe(ctx=ctx, config={ role: None }),
lambda: dev_create(ctx=ctx, config={ role: test_image }),
lambda: dev_create(ctx=ctx, config={ role: scratch_image }),
lambda: run_xfstests(ctx=ctx, config={ role: test_config }),
):
yield
@contextlib.contextmanager
def task(ctx, config):
"""