diff options
Diffstat (limited to 'test/harness/util.py')
-rw-r--r-- | test/harness/util.py | 383 |
1 files changed, 383 insertions, 0 deletions
diff --git a/test/harness/util.py b/test/harness/util.py new file mode 100644 index 0000000..f6c2a32 --- /dev/null +++ b/test/harness/util.py @@ -0,0 +1,383 @@ +""" +harness stuff (support) -- utility routines +""" + +import os +import shutil +import sys +import unittest +import time +import tempfile +import re + +from . import __version__ as lib_version + +# +# globals +# +class Global: + FSTYPE = os.getenv('FSTYPE', 'ext3') + if os.getenv('MOUNTOPTIONS'): + MOUNTOPTIONS = os.getenv('MOUNTOPTIONS').split(' ') + else: + MOUNTOPTIONS = [] + MOUNTOPTIONS += ['-t', FSTYPE] + MKFSCMD = [os.getenv('MKFSCMD', 'mkfs.' + FSTYPE)] + if os.getenv('MKFSOPTS'): + MKFSCMD += os.getenv('MKFSOPTS').split(' ') + BONNIEPARAMS = os.getenv('BONNIEPARAMS', '-r0 -n10:0:0 -s16 -uroot -f -q').split(' ') + verbosity = 1 + debug = False + # the target (e.g. "iqn.*") + target = None + # the IP and optional port (e.g. "linux-system", "192.168.10.1:3260") + ipnr = None + # the device that will be created when our target is connected + device = None + # the first and only partition on said device + partition = None + # optional override for fio disk testing block size(s) + blocksize = None + # subtests to run -- by default, all of them + # XXX we should really look how many subtests there are, but there's + # no good way to detect that. + subtest_list = [i+1 for i in range(16)] + + +def dprint(*args): + """ + Print a debug message if in debug mode + """ + if Global.debug: + print('DEBUG: ', file=sys.stderr, end='') + for arg in args: + print(arg, file=sys.stderr, end='') + print('', file=sys.stderr) + +def vprint(*args): + """ + Print a verbose message + """ + if Global.verbosity > 1 and args: + for arg in args: + print(arg, end='') + print('') + +def run_cmd(cmd, output_save_file=None): + """ + run specified command, waiting for and returning result + """ + if Global.debug: + cmd_str = ' '.join(cmd) + if output_save_file: + cmd_str += ' >& %s' % output_save_file + dprint(cmd_str) + pid = os.fork() + if pid < 0: + print("Error: cannot fork!", flie=sys.stderr) + sys.exit(1) + if pid == 0: + # the child + if output_save_file or not Global.debug: + stdout_fileno = sys.stdout.fileno() + stderr_fileno = sys.stderr.fileno() + if output_save_file: + new_stdout = os.open(output_save_file, os.O_WRONLY|os.O_CREAT|os.O_TRUNC, + mode=0o664) + else: + new_stdout = os.open('/dev/null', os.O_WRONLY) + os.dup2(new_stdout, stdout_fileno) + os.dup2(new_stdout, stderr_fileno) + os.execvp(cmd[0], cmd) + # not reached + sys.exit(1) + + # the parent + wpid, wstat = os.waitpid(pid, 0) + if wstat != 0: + dprint("exit status: (%d) %d" % (wstat, os.WEXITSTATUS(wstat))) + return os.WEXITSTATUS(wstat) + +def new_initArgParsers(self): + """ + Add some options to the normal unittest main options + """ + global old_initArgParsers + + old_initArgParsers(self) + self._main_parser.add_argument('-d', '--debug', dest='debug', + action='store_true', + help='Enable developer debugging') + self._main_parser.add_argument('-t', '--target', dest='target', + action='store', + help='Required: target name') + self._main_parser.add_argument('-i', '--ipnr', dest='ipnr', + action='store', + help='Required: name-or-ip[:port]') + self._main_parser.add_argument('-D', '--device', dest='device', + action='store', + help='Required: device') + self._main_parser.add_argument('-B', '--blocksize', dest='blocksize', + action='store', + help='block size (defaults to an assortment of sizes)') + self._main_parser.add_argument('-V', '--version', dest='version_request', + action='store_true', + help='Display Version info and exit') + self._main_parser.add_argument('-l', '--list', dest='list_tests', + action='store_true', + help='List test cases and exit') + self._main_parser.add_argument('-s', '--subtests', dest='subtest_list', + action='store', + help='Subtests to execute [default all, i.e. "1-16"]') + +def print_suite(suite): + """Print a list of tests from a test suite""" + dprint("print_suite: entering for", suite) + if hasattr(suite, '__iter__'): + for x in suite: + print_suite(x) + else: + print(suite) + +def new_parseArgs(self, argv): + """ + Gather globals from unittest main for local consumption -- this + called to parse then validate the arguments, inside each TestCase + instance. + """ + global old_parseArgs, prog_name, parent_version, lib_version + + # actually parse the arguments + old_parseArgs(self, argv) + + # now validate stuff + if self.version_request: + print('%s Version %s, harnes version %s' % \ + (prog_name, parent_version, lib_version)) + sys.exit(0) + Global.verbosity = self.verbosity + Global.debug = self.debug + if self.list_tests: + print_suite(unittest.defaultTestLoader.discover('.')) + sys.exit(0) + for v in ['target', 'ipnr', 'device']: + if getattr(self, v) is None: + print('Error: "%s" required' % v.upper()) + sys.exit(1) + setattr(Global, v, getattr(self, v)) + Global.blocksize = self.blocksize + dprint("found: verbosity=%d, target=%s, ipnr=%s, device=%s, bs=%s" % \ + (Global.verbosity, Global.target, Global.ipnr, Global.device, Global.blocksize)) + # get partition from path + device_dir = os.path.dirname(Global.device) + if device_dir == '/dev': + Global.partition = '%s1' % Global.device + elif device_dir in ['/dev/disk/by-id', '/dev/disk/by-path']: + Global.partition = '%s-part1' % Global.device + else: + print('Error: must start with "/dev" or "/dev/disk/by-{id,path}": %s' % \ + Global.device, file=sys.sttderr) + sys.exit(1) + if self.subtest_list: + if not user_spec_to_list(self.subtest_list): + self._print_help() + sys.exit(1) + +def user_spec_to_list(user_spec): + """ + We have 16 subtests. By default, we run them all, but if + the user has specified a subset, like 'N' or 'N-M', then + a list of the indicies they requested. + + XXX: expand to handle groups, e.g. 1,3-4,12 ??? + + XXX: should we validate that the range will work, or just + let an exception happen in that case? + """ + pat_single = re.compile(r'(\d+)$') + pat_range = re.compile(r'(\d+)-(\d+)$') + found = False + start_idx = None + end_idx = None + res = pat_range.match(user_spec) + if res: + # user wants just one subtest + start_idx = int(res.group(1)) - 1 + end_idx = int(res.group(2)) + dprint("Found request for range: %d-%d" % (start_idx, end_idx)) + found = True + else: + res = pat_single.match(user_spec) + if res: + start_idx = int(res.group(1)) - 1 + end_idx = start_idx + 1 + dprint("Found request for single: %d-%d" % (start_idx, end_idx)) + found = True + if not found: + print('Error: subtest spec does not match N or N-M: %s' % user_spec) + else: + dprint("subtest_list before:", Global.subtest_list) + Global.subtest_list = Global.subtest_list[start_idx:end_idx] + dprint("subtest_list after:", Global.subtest_list) + return found + +def setup_testProgram_overrides(version_str, name): + """ + Add in special handling for a couple of the methods in TestProgram (main) + so that we can add parameters and detect some globals we care about + """ + global old_parseArgs, old_initArgParsers, parent_version, prog_name + + old_initArgParsers = unittest.TestProgram._initArgParsers + unittest.TestProgram._initArgParsers = new_initArgParsers + old_parseArgs = unittest.TestProgram.parseArgs + unittest.TestProgram.parseArgs = new_parseArgs + parent_version = version_str + prog_name = name + +def verify_needed_commands_exist(cmd_list): + """ + Verify that the commands in the supplied list are in our path + """ + path_list = os.getenv('PATH').split(':') + any_cmd_not_found = False + for cmd in cmd_list: + found = False + for a_path in path_list: + if os.path.exists('%s/%s' % (a_path, cmd)): + found = True + break + if not found: + print('Error: %s must be in your PATH' % cmd) + any_cmd_not_found = True + if any_cmd_not_found: + sys.exit(1) + + +def run_fio(): + """ + Run the fio benchmark for various block sizes. + + Return zero for success. + Return non-zero for failure and a failure reason. + + Uses Globals: device, blocksize + """ + if Global.blocksize is not None: + dprint('Found a block size passed in: %s' % Global.blocksize) + blocksizes = Global.blocksize.split(' ') + else: + dprint('NO Global block size pass in?') + blocksizes = ['512', '1k', '2k', '4k', '8k', + '16k', '32k', '75536', '128k', '1000000'] + # for each block size, do a read test, then a write test + for bs in blocksizes: + vprint('Running "fio" read test: 8 threads, bs=%s' % bs) + # only support direct IO with aligned reads + if bs.endswith('k'): + direct=1 + else: + direct=0 + res = run_cmd(['fio', '--name=read-test', '--readwrite=randread', + '--runtime=2s', '--numjobs=8', '--blocksize=%s' % bs, + '--direct=%d' % direct, '--filename=%s' % Global.device]) + if res != 0: + return (res, 'fio failed') + vprint('Running "fio" write test: 8 threads, bs=%s' % bs) + res = run_cmd(['fio', '--name=write-test', '--readwrite=randwrite', + '--runtime=2s', '--numjobs=8', '--blocksize=%s' % bs, + '--direct=%d' % direct, '--filename=%s' % Global.device]) + if res != 0: + return (res, 'fio failed') + vprint('Running "fio" verify test: 1 thread, bs=%s' % bs) + res = run_cmd(['fio', '--name=verify-test', '--readwrite=randwrite', + '--runtime=2s', '--numjobs=1', '--blocksize=%s' % bs, + '--direct=%d' % direct, '--filename=%s' % Global.device, + '--verify=md5', '--verify_state_save=0']) + if res != 0: + return (res, 'fio failed') + return (0, 'Success') + +def wait_for_path(path, present=True, amt=10): + """Wait until a path exists or is gone""" + dprint("Looking for path=%s, present=%s" % (path, present)) + for i in range(amt): + time.sleep(1) + if os.path.exists(path) == present: + dprint("We are Happy :) present=%s, cnt=%d" % (present, i)) + return True + dprint("We are not happy :( present=%s actual=%s after %d seconds" % \ + (present, os.path.exists(path), amt)) + return False + +def wipe_disc(): + """ + Wipe the label and partition table from the disc drive -- the sleep-s + are needed to give the async OS and udev a chance to notice the partition + table has been erased + """ + # zero out the label and parition table + vprint('Running "sgdisk" and "dd" to wipe disc label, partitions, and filesystem') + time.sleep(1) + res = run_cmd(['sgdisk', '-Z', Global.device]) + if res != 0: + return (res, '%s: could not zero out label: %d' % (Global.device, res)) + res = run_cmd(['dd', 'if=/dev/zero', 'of=%s' % Global.device, 'bs=256k', 'count=20', 'oflag=direct']) + if res != 0: + return (res, '%s: could not zero out filesystem: %d' % (Global.device, res)) + return (0, 'Success') + +def run_parted(): + """ + Run the parted program to ensure there is one partition, + and that it covers the whole disk + + Return zero for success and the device pathname. + Return non-zero for failure and a failure reason. + + Uses Globals: device, partition + """ + (res, reason) = wipe_disc() + if res != 0: + return (res, resason) + # ensure our partition file is not there, to be safe + if not wait_for_path(Global.partition, present=False, amt=30): + return (1, '%s: Partition already exists?' % Global.partition) + # make a label, then a partition table with one partition + vprint('Running "parted" to create a label and partition table') + res = run_cmd(['parted', Global.device, 'mklabel', 'gpt']) + if res != 0: + return (res, '%s: Could not create a GPT label' % Global.device) + res = run_cmd(['parted', '-a', 'none', Global.device, 'mkpart', 'primary', '0', '100%']) + if res != 0: + return (res, '%s: Could not create a primary partition' % Global.device) + # wait for the partition to show up + if not wait_for_path(Global.partition): + return (1, '%s: Partition never showed up?' % Global.partition) + # success + return (0, 'Success') + +def run_mkfs(): + vprint('Running "mkfs" to to create filesystem') + res = run_cmd(Global.MKFSCMD + [ Global.partition ] ) + if res != 0: + return (res, '%s: mkfs failed (%d)' % (Global.partition, res)) + return (0, 'Success') + +def run_bonnie(): + # make a temp dir and mount the device there + with tempfile.TemporaryDirectory() as tmp_dir: + vprint('Mounting the filesystem') + res = run_cmd(['mount'] + Global.MOUNTOPTIONS + [Global.partition, tmp_dir]) + if res != 0: + return (res, '%s: mount failed (%d)' % (Global.partition, res)) + # run bonnie++ on the new directory + vprint('Running "bonnie++" on the filesystem') + res = run_cmd(['bonnie++'] + Global.BONNIEPARAMS + ['-d', tmp_dir]) + if res != 0: + return (res, '%s: umount failed (%d)' % (tmp_dir, res)) + # unmount the device and remove the temp dir + res = run_cmd(['umount', tmp_dir]) + if res != 0: + return (res, '%s: umount failed (%d)' % (tmp_dir, res)) + return (0, 'Success') |