Refactored to include varios reports and checks
diff --git a/ci_checker/reporter.py b/ci_checker/reporter.py
new file mode 100644
index 0000000..326969e
--- /dev/null
+++ b/ci_checker/reporter.py
@@ -0,0 +1,179 @@
+import jinja2
+import six
+import abc
+import os
+
+from ci_checker.common import const
+
+pkg_dir = os.path.dirname(__file__)
+pkg_dir = os.path.join(pkg_dir, os.pardir)
+pkg_dir = os.path.normpath(pkg_dir)
+
+
+def shortname(node_fqdn):
+    # form shortname out of node fqdn
+    return node_fqdn.split(".", 1)[0]
+
+
+def is_equal(pkg_dict):
+    # compare versions of given package
+    return pkg_dict['installed'] == pkg_dict['candidate']
+
+
+def is_active(node_dict):
+    # check node status in node dict
+    return node_dict['status'] == const.NODE_UP
+
+
+def line_breaks(text):
+    # replace python linebreaks with html breaks
+    return text.replace("\n", "<br />")
+
+
+@six.add_metaclass(abc.ABCMeta)
+class _Base(object):
+    def __init__(self):
+        self.jinja2_env = self.init_jinja2_env()
+
+    @abc.abstractmethod
+    def __call__(self, payload):
+        pass
+
+    @staticmethod
+    def init_jinja2_env():
+        return jinja2.Environment(
+            loader=jinja2.FileSystemLoader(os.path.join(pkg_dir, 'templates')),
+            trim_blocks=True,
+            lstrip_blocks=True)
+
+
+class _TMPLBase(_Base):
+    @abc.abstractproperty
+    def tmpl(self):
+        pass
+
+    @staticmethod
+    def _count_totals(data):
+        data['counters']['total_nodes'] = len(data['nodes'])
+
+    def __call__(self, nodes={}, mdl_diff={}):
+        # init data structures
+        data = self.common_data()
+        data.update({
+            "nodes": nodes,
+            "compare": data
+        })
+
+        # add template specific data
+        self._extend_data(data)
+
+        # do counts global
+        self._count_totals(data)
+
+        # specific filters
+        self.jinja2_env.filters['shortname'] = shortname
+        self.jinja2_env.filters['is_equal'] = is_equal
+        self.jinja2_env.filters['is_active'] = is_active
+        self.jinja2_env.filters['linebreaks'] = line_breaks
+
+        # render!
+        tmpl = self.jinja2_env.get_template(self.tmpl)
+        return tmpl.render(data)
+
+    def common_data(self):
+        return {
+            'counters': {},
+            'salt_info': {}
+        }
+
+    def _extend_data(self, data):
+        pass
+
+
+# Package versions report
+class HTMLPackageCandidates(_TMPLBase):
+    tmpl = "pkg_versions_tmpl.j2"
+
+    @staticmethod
+    def is_fail_uniq(p_dict, p_name, nodes, node_name):
+        # look up package fail for nodes with similar role
+        _tgroup = nodes[node_name]['node_group']
+        # filter all nodes with the same role
+        _nodes_list = filter(
+            lambda nd: nodes[nd]['node_group'] == _tgroup and nd != node_name,
+            nodes
+        )
+        # lookup same package
+        _fail_uniq = False
+        for _node_name in _nodes_list:
+            # check if there is a package present on node
+            _nd = nodes[_node_name]['packages']
+            if p_name not in _nd:
+                continue
+            # if both backages has same version and differ from candidate
+            if p_dict['candidate'] == _nd[p_name]['candidate'] \
+                    and _nd[p_name]['candidate'] == _nd[p_name]['installed']:
+                # it is not uniq, mark and break
+                _fail_uniq = True
+        return _fail_uniq
+
+    def _extend_data(self, data):
+        _all_pkg = 0
+        for key, value in data['nodes'].iteritems():
+            # add count of packages for this node to total
+            _all_pkg += len(value.keys())
+
+            # count differences
+            data['counters'][key] = {}
+            data['counters'][key]['packages'] = len(value['packages'].keys())
+            data['counters'][key]['package_diff'] = 0
+            for pkg_name, pkg_value in value['packages'].iteritems():
+                if pkg_value['installed'] != pkg_value['candidate']:
+                    pkg_value['is_equal'] = False
+                    pkg_value['fail_uniq'] = self.is_fail_uniq(
+                        pkg_value,
+                        pkg_name,
+                        data['nodes'],
+                        key
+                    )
+                    data['counters'][key]['package_diff'] += 1
+                else:
+                    pkg_value['is_equal'] = True
+                    pkg_value['fail_uniq'] = False
+
+        data['counters']['total_packages'] = _all_pkg
+
+
+# Package versions report
+class HTMLModelCompare(_TMPLBase):
+    tmpl = "model_tree_cmp_tmpl.j2"
+
+    def _extend_data(self, data):
+        # extend data with the compare dict
+
+        # counters - mdl_diff
+        data['counters']['mdl_diff'] = 51
+
+        pass
+
+
+class ReportToFile(object):
+    def __init__(self, report, target):
+        self.report = report
+        self.target = target
+
+    def __call__(self, payload):
+        payload = self.report(payload)
+
+        if isinstance(self.target, six.string_types):
+            self._wrapped_dump(payload)
+        else:
+            self._dump(payload, self.target)
+
+    def _wrapped_dump(self, payload):
+        with open(self.target, 'wt') as target:
+            self._dump(payload, target)
+
+    @staticmethod
+    def _dump(payload, target):
+        target.write(payload)