Move to new sensor selector, fix some bugs
diff --git a/wally/main.py b/wally/main.py
index 66e39db..497bdac 100644
--- a/wally/main.py
+++ b/wally/main.py
@@ -10,11 +10,9 @@
 from typing import List, Tuple, Any, Callable, IO, cast, Optional, Iterator
 from yaml import load as _yaml_load
 
-
 YLoader = Callable[[IO], Any]
 yaml_load = None  # type: YLoader
 
-
 try:
     from yaml import CLoader
     yaml_load = cast(YLoader,  functools.partial(_yaml_load, Loader=CLoader))
@@ -31,6 +29,7 @@
 
 from cephlib.common import setup_logging
 from cephlib.storage import make_storage
+from cephlib.wally_storage import WallyDB
 from cephlib.ssh import set_ssh_key_passwd
 from cephlib.node import log_nodes_statistic
 from cephlib.node_impl import get_rpc_server_code
@@ -69,29 +68,53 @@
         raise
 
 
-def list_results(path: str) -> List[Tuple[str, str, str, str]]:
-    results = []  # type: List[Tuple[float, str, str, str, str]]
-
+def list_results(path: str, limit: int = None) -> List[Tuple[str, str, str, str, str]]:
+    dirs = []
     for dir_name in os.listdir(path):
         full_path = os.path.join(path, dir_name)
+        dirs.append((os.stat(full_path).st_ctime, full_path))
 
+    dirs.sort()
+    results = []  # type: List[Tuple[str, str, str, str, str]]
+    for _, full_path in dirs[::-1]:
         try:
             stor = make_storage(full_path, existing=True)
         except Exception as exc:
             logger.warning("Can't load folder {}. Error {}".format(full_path, exc))
 
-        comment = cast(str, stor.get('info/comment'))
-        run_uuid = cast(str, stor.get('info/run_uuid'))
-        run_time = cast(float, stor.get('info/run_time'))
-        test_types = ""
-        results.append((run_time,
-                        run_uuid,
-                        test_types,
-                        time.ctime(run_time),
-                        '-' if comment is None else comment))
+        try:
+            try:
+                cfg = stor.load(Config, WallyDB.config)
+            except KeyError:
+                cfg = stor.load(Config, "config")
+        except Exception as exc:
+            print("Fail to load {}. {}".format(os.path.basename(full_path), exc))
+            continue
 
-    results.sort()
-    return [i[1:] for i in results]
+        if WallyDB.run_interval in stor:
+            run_time = stor.get(WallyDB.run_interval)[0]
+        else:
+            run_time = os.stat(full_path).st_ctime
+
+        ftime = time.strftime("%d %b %H:%M", time.localtime(run_time))
+
+        test_types = []
+        for suite_cfg in cfg.get('tests', []):
+            for suite_name, params in suite_cfg.items():
+                if suite_name == 'fio':
+                    test_types.append("{}.{}".format(suite_name, params['load']))
+                else:
+                    test_types.append(suite_name)
+        results.append((cfg.run_uuid,
+                        ",".join(test_types),
+                        ftime,
+                        '-' if cfg.comment is None else cfg.comment,
+                        '-'))
+
+        if limit and len(results) >= limit:
+            break
+
+    return results
 
 
 def log_nodes_statistic_stage(ctx: TestRun) -> None:
@@ -103,7 +126,8 @@
     parser = argparse.ArgumentParser(prog='wally', description=descr)
     parser.add_argument("-l", '--log-level', help="print some extra log info")
     parser.add_argument("--ssh-key-passwd", default=None, help="Pass ssh key password")
-    parser.add_argument("--ssh-key-passwd-kbd", action="store_true", help="Enter ssh key password interactivelly")
+    parser.add_argument("--ssh-key-passwd-kbd", action="store_true", help="Enter ssh key password interactively")
+    parser.add_argument("--profile", action="store_true", help="Profile execution")
     parser.add_argument("-s", '--settings-dir', default=None,
                         help="Folder to store key/settings/history files")
 
@@ -111,6 +135,8 @@
 
     # ---------------------------------------------------------------------
     report_parser = subparsers.add_parser('ls', help='list all results')
+    report_parser.add_argument("-l", "--limit", metavar='LIMIT', help="Show only LIMIT last results",
+                               default=None, type=int)
     report_parser.add_argument("result_storage", help="Folder with test results")
 
     # ---------------------------------------------------------------------
@@ -236,6 +262,13 @@
     config = None  # type: Config
     storage = None  # type: IStorage
 
+    if opts.profile:
+        import cProfile
+        pr = cProfile.Profile()
+        pr.enable()
+    else:
+        pr = None
+
     if opts.subparser_name == 'test':
         config = load_config(opts.config_file)
         config.storage_url, config.run_uuid = utils.get_uniq_path_uuid(config.results_storage)
@@ -250,7 +283,7 @@
         config.discover = set(name for name in config.get('discover', '').split(",") if name)
 
         storage = make_storage(config.storage_url)
-        storage.put(config, 'config')
+        storage.put(config, WallyDB.config)
 
         stages.extend(get_run_stages())
         stages.append(SaveNodesStage())
@@ -267,7 +300,7 @@
     elif opts.subparser_name == 'resume':
         opts.resumed = True
         storage = make_storage(opts.storage_dir, existing=True)
-        config = storage.load(Config, 'config')
+        config = storage.load(Config, WallyDB.config)
         stages.extend(get_run_stages())
         stages.append(LoadStoredNodesStage())
         prev_opts = storage.get('cli')  # type: List[str]
@@ -281,9 +314,10 @@
 
     elif opts.subparser_name == 'ls':
         tab = Texttable(max_width=200)
-        tab.set_cols_align(["l", "l", "l", "l"])
-        tab.header(["Name", "Tests", "Run at", "Comment"])
-        tab.add_rows(list_results(opts.result_storage))
+        tab.set_cols_align(["l", "l", "l", "l", 'c'])
+        tab.set_deco(Texttable.VLINES | Texttable.BORDER | Texttable.HEADER)
+        tab.header(["Name", "Tests", "Started at", "Comment", "Result"])
+        tab.add_rows(list_results(opts.result_storage, opts.limit), header=False)
         print(tab.draw())
         return 0
 
@@ -292,7 +326,7 @@
             print(" --no-report option can't be used with 'report' cmd")
             return 1
         storage = make_storage(opts.data_dir, existing=True)
-        config = storage.load(Config, 'config')
+        config = storage.load(Config, WallyDB.config)
         report_profiles.default_format = opts.format
         report.default_format = opts.format
         stages.append(LoadStoredNodesStage())
@@ -327,6 +361,8 @@
         print("Subparser {!r} is not supported".format(opts.subparser_name))
         return 1
 
+    start_time = int(time.time())
+
     report_stages = []  # type: List[Stage]
     if not getattr(opts, "no_report", False):
         reporters = opts.reporters.split(",")
@@ -346,6 +382,9 @@
     ctx = TestRun(config, storage, WallyStorage(storage))
     ctx.rpc_code, ctx.default_rpc_plugins = get_rpc_server_code()
 
+    if 'dev_roles' in ctx.config:
+        ctx.devs_locator = ctx.config.dev_roles
+
     if opts.ssh_key_passwd is not None:
         set_ssh_key_passwd(opts.ssh_key_passwd)
     elif opts.ssh_key_passwd_kbd:
@@ -396,16 +435,30 @@
     ctx.storage.sync()
 
     logger.info("All info is stored into %r", config.storage_url)
+    end_time = int(time.time())
+    storage.put([start_time, end_time], WallyDB.run_interval)
 
     if failed or cleanup_failed:
         if opts.subparser_name == 'report':
             logger.error("Report generation failed. See error details in log above")
         else:
             logger.error("Tests are failed. See error details in log above")
-        return 1
+        code = 1
     else:
         if opts.subparser_name == 'report':
             logger.info("Report successfully generated")
         else:
             logger.info("Tests finished successfully")
-        return 0
+        code = 0
+
+    if opts.profile:
+        assert pr is not None
+        pr.disable()
+        import pstats
+        pstats.Stats(pr).sort_stats('tottime').print_stats(30)
+
+    if opts.subparser_name == 'test':
+        storage.put(code, WallyDB.res_code)
+
+    storage.sync()
+    return code