continue refactoring for report
diff --git a/wally/suits/io/fio_job.py b/wally/suits/io/fio_job.py
new file mode 100644
index 0000000..0f55e91
--- /dev/null
+++ b/wally/suits/io/fio_job.py
@@ -0,0 +1,183 @@
+import abc
+import copy
+from collections import OrderedDict
+from typing import Optional, Iterator, Union, Dict, Tuple, NamedTuple, Any, cast
+
+
+from ...utils import ssize2b, b2ssize
+from ..job import JobConfig, JobParams
+
+
+Var = NamedTuple('Var', [('name', str)])
+
+
+def is_fio_opt_true(vl: Union[str, int]) -> bool:
+    return str(vl).lower() in ['1', 'true', 't', 'yes', 'y']
+
+
+class FioJobParams(JobParams):
+    """Class contains all parameters, which significantly affects fio results.
+
+        oper - operation type - read/write/randread/...
+        sync_mode - direct/sync/async/direct+sync
+        bsize - block size in KiB
+        qd - IO queue depth,
+        thcount - thread count,
+        write_perc - write perc for mixed(read+write) loads
+
+    Like block size or operation type, but not file name or file size.
+    Can be used as key in dictionary.
+    """
+
+    sync2long = {'x': "sync direct",
+                 's': "sync",
+                 'd': "direct",
+                 'a': "buffered"}
+
+    @property
+    def sync_mode_long(self) -> str:
+        return self.sync2long[self['sync_mode']]
+
+    @property
+    def summary(self) -> str:
+        """Test short summary, used mostly for file names and short image description"""
+        res = "{0[oper]}{0[sync_mode]}{0[bsize]}".format(self)
+        if self['qd'] is not None:
+            res += "_qd" + str(self['qd'])
+        if self['thcount'] not in (1, None):
+            res += "th" + str(self['thcount'])
+        if self['write_perc'] is not None:
+            res += "wr" + str(self['write_perc'])
+        return res
+
+    @property
+    def long_summary(self) -> str:
+        """Readable long summary for management and deployment engineers"""
+        res = "{0[sync_mode_long]} {0[oper]} {1}".format(self, b2ssize(self['bsize'] * 1024))
+        if self['qd'] is not None:
+            res += " QD = " + str(self['qd'])
+        if self['thcount'] not in (1, None):
+            res += " threads={0[thcount]}".format(self)
+        if self['write_perc'] is not None:
+            res += " write_perc={0[write_perc]}%".format(self)
+        return res
+
+
+class FioJobConfig(JobConfig):
+    """Fio job configuration"""
+    ds2mode = {(True, True): 'x',
+               (True, False): 's',
+               (False, True): 'd',
+               (False, False): 'a'}
+
+    op_type2short = {"randread": "rr",
+                     "randwrite": "rw",
+                     "read": "sr",
+                     "write": "sw",
+                     "randrw": "rx"}
+
+    def __init__(self, name: str, idx: int) -> None:
+        JobConfig.__init__(self, idx)
+        self.name = name
+        self._sync_mode = None  # type: Optional[str]
+        self._params = None  # type: Optional[Dict[str, Any]]
+
+    # ------------- BASIC PROPERTIES -----------------------------------------------------------------------------------
+
+    @property
+    def write_perc(self) -> Optional[int]:
+        try:
+            return int(self.vals["rwmixwrite"])
+        except (KeyError, TypeError):
+            try:
+                return 100 - int(self.vals["rwmixread"])
+            except (KeyError, TypeError):
+                return None
+
+    @property
+    def qd(self) -> int:
+        return int(self.vals['iodepth'])
+
+    @property
+    def bsize(self) -> int:
+        bsize = ssize2b(self.vals['blocksize'])
+        assert bsize % 1024 == 0
+        return bsize // 1024
+
+    @property
+    def oper(self) -> str:
+        return self.vals['rw']
+
+    @property
+    def op_type_short(self) -> str:
+        return self.op_type2short[self.vals['rw']]
+
+    @property
+    def thcount(self) -> int:
+        return int(self.vals.get('numjobs', 1))
+
+    @property
+    def sync_mode(self) -> str:
+        if self._sync_mode is None:
+            direct = is_fio_opt_true(self.vals.get('direct', '0')) or \
+                     not is_fio_opt_true(self.vals.get('buffered', '0'))
+            sync = is_fio_opt_true(self.vals.get('sync', '0'))
+            self._sync_mode = self.ds2mode[(sync, direct)]
+        return cast(str, self._sync_mode)
+
+    # ----------- COMPLEX PROPERTIES -----------------------------------------------------------------------------------
+
+    @property
+    def params(self) -> JobParams:
+        if self._params is None:
+            self._params = dict(oper=self.oper,
+                                sync_mode=self.sync_mode,
+                                bsize=self.bsize,
+                                qd=self.qd,
+                                thcount=self.thcount,
+                                write_perc=self.write_perc)
+        return cast(JobParams, FioJobParams(**cast(Dict[str, Any], self._params)))
+
+    # ------------------------------------------------------------------------------------------------------------------
+
+    def __eq__(self, o: object) -> bool:
+        if not isinstance(o, FioJobConfig):
+            return False
+        return self.vals == cast(FioJobConfig, o).vals
+
+    def copy(self) -> 'FioJobConfig':
+        return copy.deepcopy(self)
+
+    def required_vars(self) -> Iterator[Tuple[str, Var]]:
+        for name, val in self.vals.items():
+            if isinstance(val, Var):
+                yield name, val
+
+    def is_free(self) -> bool:
+        return len(list(self.required_vars())) == 0
+
+    def __str__(self) -> str:
+        res = "[{0}]\n".format(self.params.summary)
+
+        for name, val in self.vals.items():
+            if name.startswith('_') or name == name.upper():
+                continue
+            if isinstance(val, Var):
+                res += "{0}={{{1}}}\n".format(name, val.name)
+            else:
+                res += "{0}={1}\n".format(name, val)
+
+        return res
+
+    def __repr__(self) -> str:
+        return str(self)
+
+    def raw(self) -> Dict[str, Any]:
+        res = super().raw()
+        res['vals'] = list(map(list, self.vals.items()))
+        return res
+
+    @classmethod
+    def fromraw(cls, data: Dict[str, Any]) -> 'FioJobConfig':
+        data['vals'] = OrderedDict(data['vals'])
+        return cast(FioJobConfig, super().fromraw(data))