add rally patch-and-run script and iozone scenario
diff --git a/fix_rally.py b/fix_rally.py
new file mode 100644
index 0000000..6ab3410
--- /dev/null
+++ b/fix_rally.py
@@ -0,0 +1,220 @@
+import os
+import re
+import sys
+import time
+import yaml
+import json
+import os.path
+import datetime
+import warnings
+import functools
+import contextlib
+import multiprocessing
+
+from rally import exceptions
+from rally.cmd import cliutils
+from rally.cmd.main import categories
+from rally.benchmark.scenarios.vm.utils import VMScenario
+
+
+def log(x):
+    dt_str = datetime.datetime.now().strftime("%H:%M:%S")
+    pref = dt_str + " " + str(os.getpid()) + " >>>> "
+    sys.stderr.write(pref + x.replace("\n", "\n" + pref) + "\n")
+
+
+def get_barrier(count):
+    val = multiprocessing.Value('i', count)
+    cond = multiprocessing.Condition()
+
+    def closure(timeout):
+        with cond:
+            val.value -= 1
+
+            log("barrier value == {0}".format(val.value))
+
+            if val.value == 0:
+                cond.notify_all()
+            else:
+                cond.wait(timeout)
+            return val.value == 0
+
+    return closure
+
+
+MAX_WAIT_TOUT = 60
+
+
+@contextlib.contextmanager
+def patch_VMScenario_run_command_over_ssh(paths,
+                                          add_meta_cb,
+                                          barrier=None,
+                                          latest_start_time=None):
+
+    orig = VMScenario.run_command_over_ssh
+
+    @functools.wraps(orig)
+    def closure(self, ssh, *args, **kwargs):
+        try:
+            sftp = ssh._client.open_sftp()
+        except AttributeError:
+            # rally code was changed
+            log("Prototype of VMScenario.run_command_over_ssh "
+                "was changed. Update patch code.")
+            raise exceptions.ScriptError("monkeypath code fails on "
+                                         "ssh._client.open_sftp()")
+
+        for src, dst in paths.items():
+            try:
+                sftp.put(src, dst)
+            except Exception as exc:
+                tmpl = "Scp {0!r} => {1!r} failed - {2!r}"
+                msg = tmpl.format(src, dst, exc)
+                log(msg)
+                raise exceptions.ScriptError(
+                    "monkeypath code fails on " + msg)
+
+        sftp.close()
+
+        log("Start io test")
+
+        if barrier is not None:
+            if latest_start_time is not None:
+                timeout = latest_start_time - time.time()
+            else:
+                timeout = None
+
+            if timeout is not None and timeout > 0:
+                msg = "Ready and waiting on barrier. " + \
+                      "Will wait at most {0} seconds"
+                log(msg.format(int(timeout)))
+
+                if not barrier(timeout):
+                    log("Barrier timeouted")
+
+        try:
+            code, out, err = orig(self, ssh, *args, **kwargs)
+        except Exception as exc:
+            log("Rally raises exception {0}".format(exc.message))
+            raise
+
+        if 0 != code:
+            templ = "Script returns error! code={0}\n {1}"
+            log(templ.format(code, err.rstrip()))
+        else:
+            log("Test finished")
+            try:
+                try:
+                    result = json.loads(out)
+                except:
+                    pass
+                else:
+                    if '__meta__' in result:
+                        add_meta_cb(result.pop('__meta__'))
+                    out = json.dumps(result)
+            except Exception as err:
+                log("Error during postprocessing results: {0!r}".format(err))
+
+        return code, out, err
+
+    VMScenario.run_command_over_ssh = closure
+
+    try:
+        yield
+    finally:
+        VMScenario.run_command_over_ssh = orig
+
+
+def run_rally(rally_args):
+    return cliutils.run(['rally'] + rally_args, categories)
+
+
+def prepare_files(iozone_py_argv, dst_iozone_path, files_dir):
+
+    # we do need temporary named files
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        py_file = os.tmpnam()
+        yaml_file = os.tmpnam()
+
+    iozone_py_inp_path = os.path.join(files_dir, "iozone.py")
+    py_src_cont = open(iozone_py_inp_path).read()
+    args_repl_rr = r'INSERT_IOZONE_ARGS\(sys\.argv.*?\)'
+    py_dst_cont = re.sub(args_repl_rr, repr(iozone_py_argv), py_src_cont)
+
+    if py_dst_cont == args_repl_rr:
+        log("Can't find replace marker in file {0}".format(iozone_py_inp_path))
+        exit(1)
+
+    yaml_src_cont = open(os.path.join(files_dir, "iozone.yaml")).read()
+    task_params = yaml.load(yaml_src_cont)
+    rcd_params = task_params['VMTasks.boot_runcommand_delete']
+    rcd_params[0]['args']['script'] = py_file
+    yaml_dst_cont = yaml.dump(task_params)
+
+    open(py_file, "w").write(py_dst_cont)
+    open(yaml_file, "w").write(yaml_dst_cont)
+
+    return yaml_file, py_file
+
+
+def run_test(iozone_py_argv, dst_iozone_path, files_dir):
+    iozone_local = os.path.join(files_dir, 'iozone')
+
+    yaml_file, py_file = prepare_files(iozone_py_argv,
+                                       dst_iozone_path,
+                                       files_dir)
+
+    config = yaml.load(open(yaml_file).read())
+
+    vm_sec = 'VMTasks.boot_runcommand_delete'
+    concurrency = config[vm_sec][0]['runner']['concurrency']
+
+    max_preparation_time = 300
+
+    try:
+        copy_files = {iozone_local: dst_iozone_path}
+
+        result_queue = multiprocessing.Queue()
+        cb = result_queue.put
+
+        do_patch = patch_VMScenario_run_command_over_ssh
+
+        barrier = get_barrier(concurrency)
+        max_release_time = time.time() + max_preparation_time
+
+        with do_patch(copy_files, cb, barrier, max_release_time):
+            log("Start rally with 'task start {0}'".format(yaml_file))
+            rally_result = run_rally(['task', 'start', yaml_file])
+
+        # while not result_queue.empty():
+        #     log("meta = {0!r}\n".format(result_queue.get()))
+
+        return rally_result
+
+    finally:
+        os.unlink(yaml_file)
+        os.unlink(py_file)
+    # store and process meta and results
+
+
+def main(argv):
+    files_dir = '.'
+    dst_iozone_path = '/tmp/iozone'
+    iozone_py_argv = ['-a', 'randwrite',
+                      '--iodepth', '2',
+                      '--blocksize', '4k',
+                      '--iosize', '20M',
+                      '--iozone-path', dst_iozone_path,
+                      '-d']
+    run_test(iozone_py_argv, dst_iozone_path, files_dir)
+
+# ubuntu cloud image
+# https://cloud-images.ubuntu.com/trusty/current/trusty-server-cloudimg-amd64-disk1.img
+
+# glance image-create --name 'ubuntu' --disk-format qcow2
+# --container-format bare --is-public true --copy-from
+# https://cloud-images.ubuntu.com/trusty/current/trusty-server-cloudimg-amd64-disk1.img
+
+if __name__ == '__main__':
+    exit(main(sys.argv))
diff --git a/iozone-scenario/iozone b/iozone-scenario/iozone
new file mode 100755
index 0000000..5edce95
--- /dev/null
+++ b/iozone-scenario/iozone
Binary files differ
diff --git a/iozone-scenario/iozone.py b/iozone-scenario/iozone.py
new file mode 100644
index 0000000..5969062
--- /dev/null
+++ b/iozone-scenario/iozone.py
@@ -0,0 +1,403 @@
+import re
+import os
+import sys
+import stat
+import time
+import json
+import os.path
+import argparse
+import warnings
+import subprocess
+
+
+class BenchmarkOption(object):
+    def __init__(self, concurence, iodepth, action, blocksize, size):
+        self.iodepth = iodepth
+        self.action = action
+        self.blocksize = blocksize
+        self.concurence = concurence
+        self.size = size
+        self.direct_io = False
+        self.use_hight_io_priority = True
+        self.sync = False
+
+
+class IOZoneParser(object):
+    "class to parse iozone results"
+
+    start_tests = re.compile(r"^\s+KB\s+reclen\s+")
+    resuts = re.compile(r"[\s0-9]+")
+    mt_iozone_re = re.compile(r"\s+Children see throughput " +
+                              r"for\s+\d+\s+(?P<cmd>.*?)\s+=\s+" +
+                              r"(?P<perf>[\d.]+)\s+KB/sec")
+
+    cmap = {'initial writers': 'write',
+            'rewriters': 'rewrite',
+            'initial readers': 'read',
+            're-readers': 'reread',
+            'random readers': 'random read',
+            'random writers': 'random write'}
+
+    string1 = "                           " + \
+              "                   random  random    " + \
+              "bkwd   record   stride                                   "
+
+    string2 = "KB  reclen   write rewrite    " + \
+              "read    reread    read   write    " + \
+              "read  rewrite     read   fwrite frewrite   fread  freread"
+
+    @classmethod
+    def apply_parts(cls, parts, string, sep=' \t\n'):
+        add_offset = 0
+        for part in parts:
+            _, start, stop = part
+            start += add_offset
+            add_offset = 0
+
+            # condition splited to make pylint happy
+            while stop + add_offset < len(string):
+
+                # condition splited to make pylint happy
+                if not (string[stop + add_offset] not in sep):
+                    break
+
+                add_offset += 1
+
+            yield part, string[start:stop + add_offset]
+
+    @classmethod
+    def make_positions(cls):
+        items = [i for i in cls.string2.split() if i]
+
+        pos = 0
+        cls.positions = []
+
+        for item in items:
+            npos = cls.string2.index(item, 0 if pos == 0 else pos + 1)
+            cls.positions.append([item, pos, npos + len(item)])
+            pos = npos + len(item)
+
+        for itm, val in cls.apply_parts(cls.positions, cls.string1):
+            if val.strip():
+                itm[0] = val.strip() + " " + itm[0]
+
+    @classmethod
+    def parse_iozone_res(cls, res, mthreads=False):
+        parsed_res = None
+
+        sres = res.split('\n')
+
+        if not mthreads:
+            for pos, line in enumerate(sres[1:]):
+                if line.strip() == cls.string2 and \
+                            sres[pos].strip() == cls.string1.strip():
+                    add_pos = line.index(cls.string2)
+                    parsed_res = {}
+
+                    npos = [(name, start + add_pos, stop + add_pos)
+                            for name, start, stop in cls.positions]
+
+                    for itm, res in cls.apply_parts(npos, sres[pos + 2]):
+                        if res.strip() != '':
+                            parsed_res[itm[0]] = int(res.strip())
+
+                    del parsed_res['KB']
+                    del parsed_res['reclen']
+        else:
+            parsed_res = {}
+            for line in sres:
+                rr = cls.mt_iozone_re.match(line)
+                if rr is not None:
+                    cmd = rr.group('cmd')
+                    key = cls.cmap.get(cmd, cmd)
+                    perf = int(float(rr.group('perf')))
+                    parsed_res[key] = perf
+        return parsed_res
+
+
+def ssize_to_kb(ssize):
+    try:
+        smap = dict(k=1, K=1, M=1024, m=1024, G=1024**2, g=1024**2)
+        for ext, coef in smap.items():
+            if ssize.endswith(ext):
+                return int(ssize[:-1]) * coef
+
+        if int(ssize) % 1024 != 0:
+            raise ValueError()
+
+        return int(ssize) / 1024
+
+    except (ValueError, TypeError, AttributeError):
+        tmpl = "Unknow size format {0!r} (or size not multiples 1024)"
+        raise ValueError(tmpl.format(ssize))
+
+
+IOZoneParser.make_positions()
+
+
+def do_run_iozone(params, filename, timeout, iozone_path='iozone',
+                  microsecond_mode=False):
+
+    cmd = [iozone_path]
+
+    if params.sync:
+        cmd.append('-o')
+
+    if params.direct_io:
+        cmd.append('-I')
+
+    if microsecond_mode:
+        cmd.append('-N')
+
+    all_files = []
+    threads = int(params.concurence)
+    if 1 != threads:
+        cmd.extend(('-t', str(threads), '-F'))
+        filename = filename + "_{}"
+        cmd.extend(filename % i for i in range(threads))
+        all_files.extend(filename % i for i in range(threads))
+    else:
+        cmd.extend(('-f', filename))
+        all_files.append(filename)
+
+    bsz = 1024 if params.size > 1024 else params.size
+    if params.size % bsz != 0:
+        fsz = (params.size // bsz + 1) * bsz
+    else:
+        fsz = params.size
+
+    for fname in all_files:
+        ccmd = [iozone_path, "-f", fname, "-i", "0",
+                "-s",  str(fsz), "-r", str(bsz), "-w"]
+        subprocess.check_output(ccmd)
+
+    cmd.append('-i')
+
+    if params.action == 'write':
+        cmd.append("0")
+    elif params.action == 'randwrite':
+        cmd.append("2")
+    else:
+        raise ValueError("Unknown action {0!r}".format(params.action))
+
+    cmd.extend(('-s', str(params.size)))
+    cmd.extend(('-r', str(params.blocksize)))
+
+    raw_res = subprocess.check_output(cmd)
+
+    try:
+        parsed_res = IOZoneParser.parse_iozone_res(raw_res, threads > 1)
+
+        res = {}
+
+        if params.action == 'write':
+            res['bw_mean'] = parsed_res['write']
+        elif params.action == 'randwrite':
+            res['bw_mean'] = parsed_res['random write']
+        elif params.action == 'read':
+            res['bw_mean'] = parsed_res['read']
+        elif params.action == 'randread':
+            res['bw_mean'] = parsed_res['random read']
+    except:
+        print raw_res
+        raise
+
+    # res['bw_dev'] = 0
+    # res['bw_max'] = res["bw_mean"]
+    # res['bw_min'] = res["bw_mean"]
+
+    return res
+
+
+def run_iozone(benchmark, iozone_path, tmpname, timeout=None):
+    if timeout is not None:
+        benchmark.size = benchmark.blocksize * 50
+        res_time = do_run_iozone(benchmark, tmpname, timeout,
+                                 iozone_path=iozone_path,
+                                 microsecond_mode=True)
+
+        size = (benchmark.blocksize * timeout * 1000000)
+        size /= res_time["bw_mean"]
+        size = (size // benchmark.blocksize + 1) * benchmark.blocksize
+        benchmark.size = size
+
+    return do_run_iozone(benchmark, tmpname, timeout,
+                         iozone_path=iozone_path)
+
+
+def which(program):
+    def is_exe(fpath):
+        return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
+
+    fpath, fname = os.path.split(program)
+    if fpath:
+        if is_exe(program):
+            return program
+    else:
+        for path in os.environ["PATH"].split(os.pathsep):
+            path = path.strip('"')
+            exe_file = os.path.join(path, program)
+            if is_exe(exe_file):
+                return exe_file
+
+    return None
+
+
+def install_iozone_package():
+    if which('iozone'):
+        return
+
+    is_redhat = os.path.exists('/etc/centos-release')
+    is_redhat = is_redhat or os.path.exists('/etc/fedora-release')
+    is_redhat = is_redhat or os.path.exists('/etc/redhat-release')
+
+    if is_redhat:
+        subprocess.check_output(["yum", "install", 'iozone3'])
+        return
+
+    try:
+        os_release_cont = open('/etc/os-release').read()
+
+        is_ubuntu = "Ubuntu" in os_release_cont
+
+        if is_ubuntu or "Debian GNU/Linux" in os_release_cont:
+            subprocess.check_output(["apt-get", "install", "iozone3"])
+            return
+    except (IOError, OSError) as exc:
+        print exc
+        pass
+
+    raise RuntimeError("Unknown host OS.")
+
+
+def install_iozone_static(iozone_url, dst):
+    if not os.path.isfile(dst):
+        import urllib
+        urllib.urlretrieve(iozone_url, dst)
+
+    st = os.stat(dst)
+    os.chmod(dst, st.st_mode | stat.S_IEXEC)
+
+
+def type_size(string):
+    try:
+        return re.match("\d+[KGBM]?", string, re.I).group(0)
+    except:
+        msg = "{0!r} don't looks like size-description string".format(string)
+        raise ValueError(msg)
+
+
+def parse_args(argv):
+    parser = argparse.ArgumentParser(
+        description="Run set of `iozone` invocations and return result")
+    parser.add_argument(
+        "--iodepth", metavar="IODEPTH", type=int,
+        help="I/O depths to test in kb", required=True)
+    parser.add_argument(
+        '-a', "--action", metavar="ACTION", type=str,
+        help="actions to run", required=True,
+        choices=["read", "write", "randread", "randwrite"])
+    parser.add_argument(
+        "--blocksize", metavar="BLOCKSIZE", type=type_size,
+        help="single operation block size", required=True)
+    parser.add_argument(
+        "--timeout", metavar="TIMEOUT", type=int,
+        help="runtime of a single run", default=None)
+    parser.add_argument(
+        "--iosize", metavar="SIZE", type=type_size,
+        help="file size", default=None)
+    parser.add_argument(
+        "-s", "--sync", default=False, action="store_true",
+        help="exec sync after each write")
+    parser.add_argument(
+        "-d", "--direct-io", default=False, action="store_true",
+        help="use O_DIRECT", dest='directio')
+    parser.add_argument(
+        "-t", "--sync-time", default=None, type=int,
+        help="sleep till sime utc time", dest='sync_time')
+    parser.add_argument(
+        "--iozone-url", help="iozone static binary url",
+        dest="iozone_url", default=None)
+    parser.add_argument(
+        "--test-file", help="file path to run test on",
+        default=None, dest='test_file')
+    parser.add_argument(
+        "--iozone-path", help="iozone path",
+        default=None, dest='iozone_path')
+    return parser.parse_args(argv)
+
+
+def main(argv):
+    argv_obj = parse_args(argv)
+    argv_obj.blocksize = ssize_to_kb(argv_obj.blocksize)
+
+    if argv_obj.iosize is not None:
+        argv_obj.iosize = ssize_to_kb(argv_obj.iosize)
+
+    benchmark = BenchmarkOption(1,
+                                argv_obj.iodepth,
+                                argv_obj.action,
+                                argv_obj.blocksize,
+                                argv_obj.iosize)
+
+    benchmark.direct_io = argv_obj.directio
+
+    test_file_name = argv_obj.test_file
+    if test_file_name is None:
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            test_file_name = os.tmpnam()
+
+    remove_iozone_bin = False
+    if argv_obj.iozone_url is not None:
+        if argv_obj.iozone_path is not None:
+            sys.stderr.write("At most one option from --iozone-path and "
+                             "--iozone-url should be provided")
+            exit(1)
+        iozone_binary = os.tmpnam()
+        install_iozone_static(argv_obj.iozone_url, iozone_binary)
+        remove_iozone_bin = True
+    elif argv_obj.iozone_path is not None:
+        iozone_binary = argv_obj.iozone_path
+        if os.path.isfile(iozone_binary):
+            if not os.access(iozone_binary, os.X_OK):
+                st = os.stat(iozone_binary)
+                os.chmod(iozone_binary, st.st_mode | stat.S_IEXEC)
+
+    else:
+        iozone_binary = which('iozone')
+
+        if iozone_binary is None:
+            iozone_binary = which('iozone3')
+
+        if iozone_binary is None:
+            sys.stderr.write("Can't found neither iozone not iozone3 binary"
+                             "Provide --iozone-path or --iozone-url option")
+            exit(1)
+
+    try:
+        if argv_obj.sync_time is not None:
+            dt = argv_obj.sync_time - time.time()
+            if dt > 0:
+                time.sleep(dt)
+
+        res = run_iozone(benchmark, iozone_binary, test_file_name)
+        sys.stdout.write(json.dumps(res) + "\n")
+    finally:
+        if remove_iozone_bin:
+            os.unlink(iozone_binary)
+
+        if os.path.isfile(test_file_name):
+            os.unlink(test_file_name)
+
+
+# function-marker for patching, don't 'optimize' it
+def INSERT_IOZONE_ARGS(x):
+    return x
+
+
+if __name__ == '__main__':
+    # this line would be patched in case of run under rally
+    # don't modify it!
+    argv = INSERT_IOZONE_ARGS(sys.argv[1:])
+    exit(main(argv))
diff --git a/iozone-scenario/iozone.yaml b/iozone-scenario/iozone.yaml
new file mode 100644
index 0000000..2044281
--- /dev/null
+++ b/iozone-scenario/iozone.yaml
@@ -0,0 +1,22 @@
+---
+  VMTasks.boot_runcommand_delete:
+    -
+      args:
+        flavor:
+            name: "m1.small"
+        image:
+            name: "ubuntu"
+        floating_network: "net04_ext"
+        force_delete: false
+        script: "iozone.py"
+        interpreter: "/usr/bin/env python2"
+        username: "ubuntu"
+      runner:
+        type: "constant"
+        times: 4
+        concurrency: 4
+      context:
+        users:
+          tenants: 3
+          users_per_tenant: 2
+        network: {}
\ No newline at end of file