From f66c504430a6c568bf90a5cdf2a64eb995b557de Mon Sep 17 00:00:00 2001 From: Lee Duncan Date: Wed, 19 Feb 2020 11:14:35 -0800 Subject: Beginning to get python tests set up. Created a framework. Replacing disktest with fio, sfdisk with parted, and moving to a PyUnit test framework. --- test/.setup | 3 + test/README | 78 ++++++++++++++ test/TODO | 19 ++++ test/harness/__init__.py | 5 + test/harness/iscsi.py | 63 ++++++++++++ test/harness/util.py | 257 +++++++++++++++++++++++++++++++++++++++++++++++ test/regression.sh | 23 +++-- test/test-open-iscsi.py | 74 ++++++++++++++ 8 files changed, 512 insertions(+), 10 deletions(-) create mode 100644 test/.setup create mode 100644 test/TODO create mode 100644 test/harness/__init__.py create mode 100644 test/harness/iscsi.py create mode 100644 test/harness/util.py create mode 100755 test/test-open-iscsi.py (limited to 'test') diff --git a/test/.setup b/test/.setup new file mode 100644 index 0000000..cfa31b4 --- /dev/null +++ b/test/.setup @@ -0,0 +1,3 @@ +target="iqn.2003-01.org.linux-iscsi.linux-dell.x8664:sn.ae8d5828b4b4" +ipnr="192.168.20.3:3260" +dev="/dev/sdb" diff --git a/test/README b/test/README index 295b96f..af54c3c 100644 --- a/test/README +++ b/test/README @@ -1,3 +1,5 @@ +From the original test/README: +--------------------------------------------------------------------------- This directory contains regression suite. I would appreciate if developer will run it at least once after @@ -13,3 +15,79 @@ in current directory: Thanks! Dmitry +--------------------------------------------------------------------------- +Call: + +> # ./regression.sh -f/--format + +to run "mkfs" on the device and exit, or + +> # ./regression.sh [test#[:#]] [bsize] + +Where: + ? + ? + the device on which to test (e.g. /dev/sd?) + test#[:#] test(s) to run, i.e. single or range (default: all) + bsize disktest block size (default: a range of 10 sizes) + special value "bonnie" => skip disktest + +And env var "SKIP_WARNING" skips a big warning about writing on the device? + +--------------------------------------------------------------------------- + +The problem is that these tests have not been run for a while, and, +additionally, disktest seems to be extinct and unfindable. + +I plan to get these tests working, and consider these the steps I plan +to take: + +* understand the current tests + - particularly, what disktest and bonnie++ are doing + +* try to replace disktest and/or bonnie++, if needed + - there are a lot of good disk test packages these days + +* replace the shell code with PyTest code, to make it + easier to use + +Lee Duncan -- 2/13/2020 + +Analysis of disktest usage: + + options used: fio option for this? + ======================================= ================================== + --name=test (or whatever) + -T2 -- run 2 seconds --runtime=2 + -K8 -- run with 8 threads --numjobs=8 + -B -- set block xfer size --blocksize= (default 4k?) + -ID -- use direct IO --direct=1 + -- use device --filename= + + in read mode: + -r -- read mode --readwrite=randread + + in write mode: + -w -- write mode --readwrite=randwrite + -E 16 -- compare 16 bytes --verify=md5? (lots of options) + +It looks like the "fio" program may address these needs? + +e.g. running + +with file "test.fio": +> [test] +> rw=randread +> bs=8k +> filename=/dev/sdb +> direct=1 +> numjobs=4 +> runtime=60s + +run: + +> # fio test.fio + +The output is interactive? But results are ridiculously verbose, +but include a nice summary line or two that could be used as a +go/no-go? diff --git a/test/TODO b/test/TODO new file mode 100644 index 0000000..ce98f4e --- /dev/null +++ b/test/TODO @@ -0,0 +1,19 @@ +* get current tests running + * this will mean replacing disktest with something + (like fio?) + +* convert to PyTest insteal of shell + +* have tests create their own target using targetcli and + a file? + +* have tests do discovery themselves, instead of requiring + that to be done already + +* Augment tests + * framework for adding new tests and test types,e.g.: + - multipathing + - using interface files or not + - discovery, with and without authentication + - session creation, w/ & w/o auth + - etc diff --git a/test/harness/__init__.py b/test/harness/__init__.py new file mode 100644 index 0000000..44fd09e --- /dev/null +++ b/test/harness/__init__.py @@ -0,0 +1,5 @@ +""" +Harness functions for the open-iscsi test suite. +""" + +__version__ = "1.0" diff --git a/test/harness/iscsi.py b/test/harness/iscsi.py new file mode 100644 index 0000000..569a66e --- /dev/null +++ b/test/harness/iscsi.py @@ -0,0 +1,63 @@ +""" +ISCSI classes and utilities +""" + +from .util import * + +class IscsiData: + """ + Gather all the iscsi data in one place + """ + imm_data_en = 'Yes' + initial_r2t_en = 'No' + hdrdgst_en = 'None,CRC32C' + datdgst_en = 'None,CRC32C' + first_burst = 256 * 1024 + max_burst = 16 * 1024 * 1024 - 1024 + max_recv_dlength = 128 * 1024 + max_r2t = 1 + # the target-name and IP:Port + target = None + ipnr = None + + def __init__(self, + imm_data_en=imm_data_en, + initial_r2t_en=initial_r2t_en, + hdrdgst_en=hdrdgst_en, + datdgst_en=datdgst_en, + first_burst=first_burst, + max_burst=max_burst, + max_recv_dlength=max_recv_dlength, + max_r2t=max_r2t): + self.imm_data_en = imm_data_en + self.initial_r2t_en = initial_r2t_en + self.hdrdgst_en = hdrdgst_en + self.datdgst_en = datdgst_en + self.first_burst = first_burst + self.max_burst = max_burst + self.max_recv_dlength = max_recv_dlength + self.max_r2t = max_r2t + + def update_cfg(self, target, ipnr): + """ + Update the configuration -- we could do this by hacking on the + appropriate DB file, but this is safer (and slower) by far + """ + if Global.verbosity > 1: + print('* ImmediateData = %s' % self.imm_data_en) + print('* InitialR2T = %s' % self.initial_r2t_en) + print('* HeaderDigest = %s' % self.hdrdgst_en) + print('* DataDigest = %s' % self.datdgst_en) + print('* FirstBurstLength = %d' % self.first_burst) + print('* MaxBurstLength = %d' % self.max_burst) + print('* MaxRecvDataSegmentLength = %d' % self.max_recv_dlength) + print('* MaxOutstandingR2T = %d' % self.max_r2t) + c = ['iscsiadm', '-m', 'node', '-T', target, '-p', ipnr, '-o', 'update'] + run_cmd(c + ['-n', 'node.session.iscsi.ImmediateData', '-v', self.imm_data_en], quiet_mode=True) + run_cmd(c + ['-n', 'node.session.iscsi.InitialR2T', '-v', self.initial_r2t_en], quiet_mode=True) + run_cmd(c + ['-n', 'node.conn[0].iscsi.HeaderDigest', '-v', self.hdrdgst_en], quiet_mode=True) + run_cmd(c + ['-n', 'node.conn[0].iscsi.DataDigest', '-v', self.datdgst_en], quiet_mode=True) + run_cmd(c + ['-n', 'node.session.iscsi.FirstBurstLength', '-v', str(self.first_burst)], quiet_mode=True) + run_cmd(c + ['-n', 'node.session.iscsi.MaxBurstLength', '-v', str(self.max_burst)], quiet_mode=True) + run_cmd(c + ['-n', 'node.conn[0].iscsi.MaxRecvDataSegmentLength', '-v', str(self.max_recv_dlength)], quiet_mode=True) + run_cmd(c + ['-n', 'node.session.iscsi.MaxOutstandingR2T', '-v', str(self.max_r2t)], quiet_mode=True) diff --git a/test/harness/util.py b/test/harness/util.py new file mode 100644 index 0000000..3d21480 --- /dev/null +++ b/test/harness/util.py @@ -0,0 +1,257 @@ +""" +harness stuff (support) -- utility routines +""" + +import os +import shutil +import sys +import unittest + +# +# globals +# +class Global: + _FSTYPE = os.getenv('FSTYPE', 'ext3') + _MOUNTOPTIONS = os.getenv('MOUNTOPTIONS', '').split(' ') + [' -t ', _FSTYPE] + _MKFSCMD = [os.getenv('MKFSCMD', 'mkfs.' + _FSTYPE)] + if os.getenv('MKFSOPTS'): + _MKFSCMD += os.getenv('MKFSOPTS').split(' ') + _PARTITIONSUFFIX = '1' + _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 + + +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 notice(*args): + """ + Print if not in quiet mode + """ + if Global.verbosity > 0: + for arg in args: + print(arg, end='') + print('') + +def run_cmd(cmd, output_save_file=None, quiet_mode=False): + """ + run specified command, waiting for and returning result + """ + if (Global.verbosity > 1 and not quiet_mode) or Global.debug: + cmd_str = ' '.join(cmd) + if output_save_file: + cmd_str += ' >& %s' % output_save_file + vprint(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 quiet_mode: + 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 + + # 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)') + +def new_parseArgs(self, argv): + """ + Gather globals from unittest main for local consumption + """ + global old_parseArgs + + old_parseArgs(self, argv) + Global.verbosity = self.verbosity + Global.debug = self.debug + 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) + +def setup_testProgram_overrides(): + """ + 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 + + old_initArgParsers = unittest.TestProgram._initArgParsers + unittest.TestProgram._initArgParsers = new_initArgParsers + old_parseArgs = unittest.TestProgram.parseArgs + unittest.TestProgram.parseArgs = new_parseArgs + + +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(':') + 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) + 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: 2 seconds, 8 threads, bs=%s' % bs) + res = run_cmd(['fio', '--name=read-test', '--readwrite=randread', + '--runtime=2s', '--numjobs=8', '--blocksize=%s' % bs, + '--direct=1', '--filename=%s' % Global.device], quiet_mode=True) + if res != 0: + return (res, 'fio failed') + vprint('Running "fio" write test: 2 seconds, 8 threads, bs=%s' % bs) + res = run_cmd(['fio', '--name=write-test', '--readwrite=randwrite', + '--runtime=2s', '--numjobs=8', '--blocksize=%s' % bs, + '--direct=1', '--filename=%s' % Global.device], quiet_mode=True) + if res != 0: + return (res, 'fio failed') + vprint('Running "fio" verify test: 2 seconds, 8 threads, bs=%s' % bs) + res = run_cmd(['fio', '--name=verify-test', '--readwrite=randwrite', + '--runtime=2s', '--numjobs=1', '--blocksize=%s' % bs, + '--direct=1', '--filename=%s' % Global.device, + '--verify=md5', '--verify_state_save=0'], quiet_mode=True) + if res != 0: + return (res, 'fio failed') + return (0, None) + +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 + """ + # zero out the label and parition table + res = run_cmd(['dd', 'if=/dev/zero', 'of=%s' % Global.device, 'bs=4k', 'count=100'], + quiet_mode=True) + if res != 0: + return (res, '%s: could not zero out label' % Global.device) + # ensure our partition file is not there, to be safe + if os.path.exists(Global.partition): + return (1, '%s: Partition already exists?' % Global.partition) + # make a label, then a partition table with one partition + res = run_cmd(['parted', Global.device, 'mklabel', 'gpt'], quiet_mode=True) + 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%'], + quiet_mode=True) + if res != 0: + return (res, '%s: Could not create a primary partition' % Global.device) + # wait for the partition to show up + for i in range(10): + if os.path.exists(Global.partition): + break + os.sleep(1) + if not os.path.exists(Global.partition): + return (1, '%s: Partition never showed up?' % Global.partition) + return (0, None) + +def run_mkfs(): + #return (1, "Not Yet Implemented: mkfs") + return (0, None) + +def run_bonnie(): + #return (1, "Not Yet Implemented: bonnie") + return (0, None) diff --git a/test/regression.sh b/test/regression.sh index 25d4a28..aeef74d 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -64,13 +64,16 @@ function disktest_run() { test "x$bsize" = xbonnie && return 0; for bs in $bsizes; do echo -n "disktest -T2 -K8 -B$bs -r -ID $device: " - if ! ${disktest} -T2 -K8 -B$bs -r -ID $device >/dev/null; then + #if ! ${disktest} -T2 -K8 -B$bs -r -ID $device >/dev/null; then + if ! ${disktest} -T2 -K8 -B$bs -r -ID $device; then echo "FAILED" return 1; fi echo "PASSED" - echo -n "disktest -T2 -K8 -B$bs -E16 -w -ID $device: " - if ! ${disktest} -T2 -K8 -B$bs -E16 -w -ID $device >/dev/null;then + #echo -n "disktest -T2 -K8 -B$bs -E16 -w -ID $device: " + #if ! ${disktest} -T2 -K8 -B$bs -E16 -w -ID $device >/dev/null;then + echo -n "disktest -T2 -K8 -B$bs -E16 -ID $device: " + if ! ${disktest} -T2 -K8 -B$bs -E16 -ID $device; then echo "FAILED" return 1; fi @@ -80,12 +83,11 @@ function disktest_run() { } function fdisk_run() { - echo -n "sfdisk -Lqf $device: " - sfdisk -Lqf $device >/dev/null 2>/dev/null <<-EOF - 0, - ; - ; - ; + echo -n "sfdisk -qf $device: " + #sfdisk -Lqf $device >/dev/null 2>/dev/null <<-EOF + sfdisk -Lqf $device <<-EOF + , + quit EOF rc=$? if [ $rc -ne 0 ]; then @@ -98,7 +100,8 @@ function fdisk_run() { function mkfs_run() { echo -n "${MKFSCMD} $device_partition: " - if ! ${MKFSCMD} $device_partition 2>/dev/null >/dev/null; then + #if ! ${MKFSCMD} $device_partition 2>/dev/null >/dev/null; then + if ! ${MKFSCMD} $device_partition ; then echo "FAILED" return 1; fi diff --git a/test/test-open-iscsi.py b/test/test-open-iscsi.py new file mode 100755 index 0000000..8d5bdeb --- /dev/null +++ b/test/test-open-iscsi.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Unit tests for open-iscsi, using the unittest built-in package +""" + +import sys +import unittest +import os +from harness import util +from harness.util import Global +from harness.iscsi import IscsiData + + +class TestRegression(unittest.TestCase): + """ + Regression testing + """ + + @classmethod + def setUpClass(cls): + util.vprint('*** Starting %s' % cls.__name__) + # XXX validate that target exists? + cls.first_burst_values = [4096, 8192, 16384, 32768, 65536, 131972] + cls.max_burst_values = [4096, 8192, 16384, 32768, 65536, 131072] + cls.max_recv_values = [4096, 8192, 16384, 32768, 65536, 131072] + + def setUp(self): + if Global.debug or Global.verbosity > 1: + # this makes debug printing a little more clean + print('', file=sys.stderr) + res = util.run_cmd(['iscsiadm', '-m', 'node', '-T', Global.target, '-p', Global.ipnr, '--logout'], quiet_mode=True) + if res not in [0, 21]: + self.fail('logout failed') + self.assertFalse(os.path.exists(Global.device), '%s: exists after logout!' % Global.device) + + def test_immediate_data(self): + """ + Test No Immediate Data but Initial Request to Transmit + """ + iscsi_data = IscsiData('No', 'Yes', 'None', 'None', 4096, 4096, 4096) + iscsi_data.update_cfg(Global.target, Global.ipnr) + res = util.run_cmd(['iscsiadm', '-m', 'node', '-T', Global.target, '-p', Global.ipnr, '--login'], quiet_mode=True) + self.assertEqual(res, 0, 'cannot login to device') + # wait a few seconds for the device to show up + for i in range(10): + if os.path.exists(Global.device): + break + os.sleep(1) + self.assertTrue(os.path.exists(Global.device), '%s: does not exist after login' % Global.device) + (res, reason) = util.run_fio() + self.assertEqual(res, 0, reason) + (res, reason) = util.run_parted() + self.assertEqual(res, 0, reason) + (res, reason) = util.run_mkfs() + self.assertEqual(res, 0, reason) + (res, reason) = util.run_bonnie() + self.assertEqual(res, 0, reason) + + @classmethod + def tearDownClass(cls): + # restore iscsi config + iscsi_data = IscsiData() + iscsi_data.update_cfg(Global.target, Global.ipnr) + # log out of iscsi connection + util.run_cmd(['iscsiadm', '-m', 'node', '-T', Global.target, '-p', Global.ipnr, '--logout'], quiet_mode=True) + + +if __name__ == '__main__': + util.verify_needed_commands_exist(['parted', 'fio', 'mkfs', 'bonnie++', 'dd', 'iscsiadm']) + # do our own hackery first, to get access to verbosity, debug, etc, + # as well as add our own command-line options + util.setup_testProgram_overrides() + # now run the tests + unittest.main() -- cgit v1.2.1