pre-release updates, bug fixes
diff --git a/config.py b/config.py
index e7830bb..ab9ec8c 100644
--- a/config.py
+++ b/config.py
@@ -28,3 +28,4 @@
 
     cfg_dict['vm_ids_fname'] = os.path.join(cfg_dict['var_dir'], 'os_vm_ids')
     cfg_dict['report'] = os.path.join(cfg_dict['var_dir'], 'report.html')
+    cfg_dict['log_file'] = os.path.join(cfg_dict['var_dir'], 'log.txt')
diff --git a/koder.yaml b/koder.yaml
deleted file mode 100644
index ab8e51b..0000000
--- a/koder.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-clouds:
-    fuel:
-        ext_ip: 172.16.53.3
-        url: http://172.16.52.112:8000/
-        creds: admin:admin@admin
-        ssh_creds: root:test37
-        openstack_env: test
-
-    openstack:
-        OS_TENANT_NAME: admin
-        OS_USERNAME: admin
-        OS_PASSWORD: admin
-        OS_AUTH_URL: http://172.16.53.3:5000/v2.0/
-
-discover: fuel
-
-# explicit_nodes:
-#     "ssh://192.168.152.43": testnode
-
-internal:
-    var_dir_root: /tmp/perf_tests
-
-sensors:
-    receiver_uri: udp://192.168.152.1:5699
-    roles_mapping:
-        ceph-osd: block-io
-        testnode: system-cpu, block-io
-
-tests:
-    start_test_nodes:
-        openstack:
-            creds: clouds
-            vm_params:
-                count: x2
-                img_name: disk_io_perf
-                flavor_name: disk_io_perf.256
-                keypair_name: disk_io_perf
-                network_zone_name: novanetwork
-                flt_ip_pool: nova
-                creds: "ssh://ubuntu@{0}::disk_io_perf.pem"
-                name_templ: disk_io_perf-{0}
-
-        tests:
-            - io:
-                cfg: tests/io_task_test.cfg
-                params:
-                    FILENAME: /tmp/xxx.bin
-
-logging:
-    extra_logs: 1
diff --git a/pretty_yaml.py b/pretty_yaml.py
new file mode 100644
index 0000000..44d4e49
--- /dev/null
+++ b/pretty_yaml.py
@@ -0,0 +1,75 @@
+def dumps_simple(val):
+    bad_symbols = set(" \r\t\n,':")
+
+    if isinstance(val, basestring):
+        if len(bad_symbols & set(val)) != 0:
+            return repr(val)
+        return val
+    elif val is True:
+        return 'true'
+    elif val is False:
+        return 'false'
+    elif val is None:
+        return 'null'
+
+    return str(val)
+
+
+def is_simple(val):
+    simple_type = isinstance(val, (str, unicode, int, long, bool, float))
+    return simple_type or val is None
+
+
+def all_nums(vals):
+    return all(isinstance(val, (int, float, long)) for val in vals)
+
+
+def dumpv(data, tab_sz=4, width=120, min_width=40):
+    tab = ' ' * tab_sz
+
+    if width < min_width:
+        width = min_width
+
+    res = []
+    if is_simple(data):
+        return [dumps_simple(data)]
+
+    if isinstance(data, (list, tuple)):
+        if all(map(is_simple, data)):
+            if all_nums(data):
+                one_line = "[{}]".format(", ".join(map(dumps_simple, data)))
+            else:
+                one_line = "[{}]".format(",".join(map(dumps_simple, data)))
+        else:
+            one_line = None
+
+        if one_line is None or len(one_line) > width:
+            pref = "-" + ' ' * (tab_sz - 1)
+
+            for val in data:
+                items = dumpv(val, tab_sz, width - tab_sz, min_width)
+                items = [pref + items[0]] + \
+                        [tab + item for item in items[1:]]
+                res.extend(items)
+        else:
+            res.append(one_line)
+    elif isinstance(data, dict):
+        assert all(map(is_simple, data.keys()))
+
+        for k, v in data.items():
+            key_str = dumps_simple(k) + ": "
+            val_res = dumpv(v, tab_sz, width - tab_sz, min_width)
+
+            if len(val_res) == 1 and len(key_str + val_res[0]) < width:
+                res.append(key_str + val_res[0])
+            else:
+                res.append(key_str)
+                res.extend(tab + i for i in val_res)
+    else:
+        raise ValueError("Can't pack {0!r}".format(data))
+
+    return res
+
+
+def dumps(data, tab_sz=4, width=120, min_width=40):
+    return "\n".join(dumpv(data, tab_sz, width, min_width))
diff --git a/report.html b/report.html
deleted file mode 100644
index b82cbbd..0000000
--- a/report.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <title>Report</title>
-</head>
-
-<body>
-%(body)s
-</body>
-
-</html>
\ No newline at end of file
diff --git a/run_test.py b/run_test.py
index 1841bc8..2e01394 100755
--- a/run_test.py
+++ b/run_test.py
@@ -28,19 +28,21 @@
 logger = logging.getLogger("io-perf-tool")
 
 
-def setup_logger(logger, level=logging.DEBUG):
-    logger.setLevel(level)
-    ch = logging.StreamHandler()
-    ch.setLevel(level)
-    logger.addHandler(ch)
+def setup_logger(logger, level=logging.DEBUG, log_fname=None):
+    # logger.setLevel(level)
+    sh = logging.StreamHandler()
+    sh.setLevel(level)
 
     log_format = '%(asctime)s - %(levelname)-6s - %(name)s - %(message)s'
     formatter = logging.Formatter(log_format,
                                   "%H:%M:%S")
-    ch.setFormatter(formatter)
+    sh.setFormatter(formatter)
+    logger.addHandler(sh)
 
-    # logger.setLevel(logging.INFO)
-    # logger.addHandler(logging.FileHandler('log.txt'))
+    if log_fname is not None:
+        fh = logging.FileHandler(log_fname)
+        fh.setLevel(logging.DEBUG)
+        logger.addHandler(fh)
 
 
 def format_result(res, formatter):
@@ -403,10 +405,11 @@
             report_stage
         ]
 
-    level = logging.DEBUG if opts.extra_logs else logging.WARNING
-    setup_logger(logger, level)
-
     load_config(opts.config_file)
+
+    level = logging.DEBUG if opts.extra_logs else logging.WARNING
+    setup_logger(logger, level, cfg_dict['log_file'])
+
     logger.info("Store all info into {0}".format(cfg_dict['var_dir']))
 
     ctx = Context()
diff --git a/tests/fio_configs/1.cfg b/scripts/fio_tests_configs/1.cfg
similarity index 100%
rename from tests/fio_configs/1.cfg
rename to scripts/fio_tests_configs/1.cfg
diff --git a/tests/fio_configs/2.cfg b/scripts/fio_tests_configs/2.cfg
similarity index 100%
rename from tests/fio_configs/2.cfg
rename to scripts/fio_tests_configs/2.cfg
diff --git a/tests/io_task.cfg b/scripts/fio_tests_configs/io_task.cfg
similarity index 100%
rename from tests/io_task.cfg
rename to scripts/fio_tests_configs/io_task.cfg
diff --git a/tests/io_task_test.cfg b/scripts/fio_tests_configs/io_task_test.cfg
similarity index 100%
rename from tests/io_task_test.cfg
rename to scripts/fio_tests_configs/io_task_test.cfg
diff --git a/tests.yaml b/scripts/tests.yaml
similarity index 100%
rename from tests.yaml
rename to scripts/tests.yaml
diff --git a/sensors/storage/koder.js b/sensors/storage/koder.js
new file mode 100644
index 0000000..a65a454
--- /dev/null
+++ b/sensors/storage/koder.js
@@ -0,0 +1,47 @@
+/* global _ */
+
+/*
+ * Complex scripted dashboard
+ * This script generates a dashboard object that Grafana can load. It also takes a number of user
+ * supplied URL parameters (int ARGS variable)
+ *
+ * Return a dashboard object, or a function
+ *
+ * For async scripts, return a function, this function must take a single callback function as argument,
+ * call this callback function with the dashboard object (look at scripted_async.js for an example)
+ */
+
+
+
+// accessable variables in this scope
+var window, document, ARGS, $, jQuery, moment, kbn;
+
+// Setup some variables
+var dashboard;
+
+// All url parameters are available via the ARGS object
+var ARGS;
+
+// Intialize a skeleton with nothing but a rows array and service object
+dashboard = {rows : []};
+
+// Set a title
+dashboard.title = 'Tests dash';
+
+// Set default time
+// time can be overriden in the url using from/to parameteres, but this is
+// handled automatically in grafana core during dashboard initialization
+dashboard.time = {
+    from: "now-5m",
+    to: "now"
+};
+
+dashboard.rows.push({
+    title: 'Chart',
+    height: '300px',
+    panels: [{"span": 12, "title": "writes_completed", "linewidth": 2, "type": "graph", "targets": [{"alias": "192.168.0.104 io sda1", "interval": "", "target": "disk io", "rawQuery": true, "query": "select value from \"writes_completed\" where $timeFilter and host='192.168.0.104' and device='sda1' order asc"}, {"alias": "192.168.0.104 io rbd1", "interval": "", "target": "disk io", "rawQuery": true, "query": "select value from \"writes_completed\" where $timeFilter and host='192.168.0.104' and device='rbd1' order asc"}], "tooltip": {"shared": true}, "fill": 1}, {"span": 12, "title": "sectors_written", "linewidth": 2, "type": "graph", "targets": [{"alias": "192.168.0.104 io sda1", "interval": "", "target": "disk io", "rawQuery": true, "query": "select value from \"sectors_written\" where $timeFilter and host='192.168.0.104' and device='sda1' order asc"}, {"alias": "192.168.0.104 io rbd1", "interval": "", "target": "disk io", "rawQuery": true, "query": "select value from \"sectors_written\" where $timeFilter and host='192.168.0.104' and device='rbd1' order asc"}], "tooltip": {"shared": true}, "fill": 1}]
+});
+
+
+return dashboard;
+