Packages report updates

- All Errors are warnings by default
- If package version differs across nodes
  warning becomes error

Change-Id: I1e6d338cfae252cc5d8ee6ededdd757ec070eb2c
Related-PROD: PROD-38972
diff --git a/cfg_checker/common/const.py b/cfg_checker/common/const.py
index 5826f43..9e5fea2 100644
--- a/cfg_checker/common/const.py
+++ b/cfg_checker/common/const.py
@@ -15,6 +15,7 @@
 VERSION_OK = next(_cnt)
 VERSION_UP = next(_cnt)
 VERSION_DOWN = next(_cnt)
+VERSION_WARN = next(_cnt)
 VERSION_ERR = next(_cnt)
 
 # action const order is important!
@@ -39,6 +40,7 @@
     VERSION_OK: "ok",
     VERSION_UP: "upgraded",
     VERSION_DOWN: "downgraded",
+    VERSION_WARN: "warning",
     VERSION_ERR: "error",
     VERSION_NA: "no status"
 }
@@ -65,6 +67,8 @@
     "mon": "monitoring",
     "msg": "messaging",
     "mtr": "stacklight_metering",
+    "ntw": "contrail_networking",
+    "nal": "contrail_analytics",
     "osd": "storage_node",
     "prx": "proxy",
     "rgw": "storage_rados",
diff --git a/cfg_checker/common/other.py b/cfg_checker/common/other.py
index 2620d05..c5ad7c8 100644
--- a/cfg_checker/common/other.py
+++ b/cfg_checker/common/other.py
@@ -20,6 +20,35 @@
     return _ps
 
 
+def merge_dict(source, destination):
+    """
+    Dict merger, thanks to vincent
+    http://stackoverflow.com/questions/20656135/python-deep-merge-dictionary-data
+
+    >>> a = { 'first' : { 'all_rows' : { 'pass' : 'dog', 'number' : '1' } } }
+    >>> b = { 'first' : { 'all_rows' : { 'fail' : 'cat', 'number' : '5' } } }
+    >>> merge(b, a) == {
+        'first': {
+            'all_rows': {
+                'pass': 'dog',
+                'fail': 'cat',
+                'number': '5'
+            }
+        }
+    }
+    True
+    """
+    for key, value in source.items():
+        if isinstance(value, dict):
+            # get node or create one
+            node = destination.setdefault(key, {})
+            merge_dict(value, node)
+        else:
+            destination[key] = value
+
+    return destination
+
+
 class Utils(object):
     @staticmethod
     def validate_name(fqdn, message=False):
diff --git a/cfg_checker/modules/packages/checker.py b/cfg_checker/modules/packages/checker.py
index 631c992..92d9e1c 100644
--- a/cfg_checker/modules/packages/checker.py
+++ b/cfg_checker/modules/packages/checker.py
@@ -2,6 +2,7 @@
 
 from cfg_checker.common import const, logger_cli
 from cfg_checker.common.exception import ConfigException
+from cfg_checker.common.other import merge_dict
 from cfg_checker.helpers.console_utils import Progress
 from cfg_checker.modules.packages.repos import RepoManager
 from cfg_checker.nodes import salt_master
@@ -42,6 +43,7 @@
                 "ok": const.VERSION_OK,
                 "up": const.VERSION_UP,
                 "down": const.VERSION_DOWN,
+                "warn": const.VERSION_WARN,
                 "err": const.VERSION_ERR
             },
             "ca": {
@@ -53,6 +55,7 @@
             }
         }
         _data['status_err'] = const.VERSION_ERR
+        _data['status_warn'] = const.VERSION_WARN
         _data['status_down'] = const.VERSION_DOWN
 
         # Presort packages
@@ -66,6 +69,7 @@
         _progress_index = 0
         # counters
         _ec = _es = _eo = _eu = 0
+        _wc = _ws = _wo = _wu = 0
         _dc = _ds = _do = _du = 0
         while _progress_index < _l:
             # progress bar
@@ -86,12 +90,43 @@
                         # ...just skip it from report
                         continue
 
+            _differ = len(set(_val['results'].keys())) > 1
+            if _differ:
+                # in case package has different status across nodes
+                # Warning becomes Error.
+                if const.VERSION_WARN in _val['results']:
+                    if const.VERSION_ERR in _val['results']:
+                        # add warns to err
+                        # should never happen, though
+                        merge_dict(
+                            _val['results'].pop(const.VERSION_WARN),
+                            _val['results'][const.VERSION_ERR]
+                        )
+                    else:
+                        _val['results'][const.VERSION_ERR] = \
+                            _val['results'].pop(const.VERSION_WARN)
+            else:
+                # in case package has same status on all nodes
+                # Error becomes Warning
+                if const.VERSION_ERR in _val['results']:
+                    if const.VERSION_WARN in _val['results']:
+                        # add warns to err
+                        # should never happen, though
+                        merge_dict(
+                            _val['results'].pop(const.VERSION_ERR),
+                            _val['results'][const.VERSION_WARN]
+                        )
+                    else:
+                        _val['results'][const.VERSION_WARN] = \
+                            _val['results'].pop(const.VERSION_ERR)
+
             if len(_c) > 0 and _val['is_mirantis'] is None:
                 # not listed package in version lib
                 _data['unlisted'].update({
                     _pn: _val
                 })
                 _eu += _val['results'].keys().count(const.VERSION_ERR)
+                _wu += _val['results'].keys().count(const.VERSION_WARN)
                 _du += _val['results'].keys().count(const.VERSION_DOWN)
             # mirantis/critical
             # elif len(_c) > 0 and _c != 'System':
@@ -101,6 +136,7 @@
                     _pn: _val
                 })
                 _ec += _val['results'].keys().count(const.VERSION_ERR)
+                _wc += _val['results'].keys().count(const.VERSION_WARN)
                 _dc += _val['results'].keys().count(const.VERSION_DOWN)
             # system
             elif _c == 'System':
@@ -108,6 +144,7 @@
                     _pn: _val
                 })
                 _es += _val['results'].keys().count(const.VERSION_ERR)
+                _ws += _val['results'].keys().count(const.VERSION_WARN)
                 _ds += _val['results'].keys().count(const.VERSION_DOWN)
             # rest
             else:
@@ -115,6 +152,7 @@
                     _pn: _val
                 })
                 _eo += _val['results'].keys().count(const.VERSION_ERR)
+                _wo += _val['results'].keys().count(const.VERSION_WARN)
                 _do += _val['results'].keys().count(const.VERSION_DOWN)
 
         _progress.end()
@@ -125,6 +163,12 @@
             'other': _eo,
             'unlisted': _eu
         }
+        _data['warnings'] = {
+            'mirantis': _wc,
+            'system': _ws,
+            'other': _wo,
+            'unlisted': _wu
+        }
         _data['downgrades'] = {
             'mirantis': _dc,
             'system': _ds,
diff --git a/cfg_checker/modules/packages/versions.py b/cfg_checker/modules/packages/versions.py
index e16dab8..7fae9fc 100644
--- a/cfg_checker/modules/packages/versions.py
+++ b/cfg_checker/modules/packages/versions.py
@@ -303,17 +303,17 @@
                     self.action = const.ACT_UPGRADE
                 elif i < r and r < c:
                     # installed version is older vs release version
-                    self.status = const.VERSION_ERR
+                    self.status = const.VERSION_WARN
                     self.action = const.ACT_NEED_UP
                     self.target = r
                 elif i < r and c == r:
                     # installed version is older vs release version
-                    self.status = const.VERSION_ERR
+                    self.status = const.VERSION_WARN
                     self.action = const.ACT_NEED_UP
                     self.target = c
                 elif c < r:
                     # installed and repo versions older vs release version
-                    self.status = const.VERSION_ERR
+                    self.status = const.VERSION_WARN
                     self.action = const.ACT_REPO
             # I > C
             # installed version is newer
@@ -321,7 +321,7 @@
                 self.target = c
                 if c == r:
                     # some unknown version installed
-                    self.status = const.VERSION_ERR
+                    self.status = const.VERSION_WARN
                     self.action = const.ACT_NEED_DOWN
                 elif c > r:
                     # installed and repo versions newer than release
diff --git a/cfg_checker/reports/reporter.py b/cfg_checker/reports/reporter.py
index 40f2c59..2a38bd2 100644
--- a/cfg_checker/reports/reporter.py
+++ b/cfg_checker/reports/reporter.py
@@ -81,6 +81,7 @@
         const.VERSION_OK: "OK",
         const.VERSION_UP: "Upgraded",
         const.VERSION_DOWN: "Downgraded",
+        const.VERSION_WARN: "WARNING",
         const.VERSION_ERR: "ERROR",
         const.VERSION_NA: "N/A"
     }
diff --git a/templates/pkg_versions_html.j2 b/templates/pkg_versions_html.j2
index aaedcbc..736146b 100644
--- a/templates/pkg_versions_html.j2
+++ b/templates/pkg_versions_html.j2
@@ -106,10 +106,12 @@
 
         .ok {color: darkslategray;}
         .error {color: white; background-color: darkred;}
+        .warning {color: black; background-color: lightgoldenrodyellow;}
         .upgraded {color: whitesmoke; background-color: darkslategray;}
         .downgraded {color: red;}
 
         .smallgreytext {float: right; font-size: 0.5em; color: gray;}
+        .tooltiptext { top: 120%; }
 
         /* Table specific */
         .nodes tr:nth-child(even) {
@@ -235,6 +237,20 @@
             <tr><td class="note" colspan="7">no errors found </td></tr>
             {% endif %}
 
+            <!-- Print warnings -->
+            <tr><td colspan="7">Warnings ({{ warnings[id_label] }})</td></tr>
+            {% for pkg_name in pkg_dict | get_sorted_keys %}
+            {% set dat = pkg_dict[pkg_name] %}
+                {% if status_warn in dat['results'] %}
+                    {% set action_to_show = dat['results'][status_warn].keys() | get_max %}
+                    {% call render_package(pkg_name, dat, status_warn, action_to_show, id_label) %}
+                    {% endcall %}
+                {% endif%}
+            {% endfor %}
+            {% if not warnings[id_label] %}
+            <tr><td class="note" colspan="7">no warnings found </td></tr>
+            {% endif %}
+
             <!-- Print downgrades -->
             <tr><td colspan="7">Downgrades ({{ downgrades[id_label] }})</td></tr>
             {% for pkg_name in pkg_dict | get_sorted_keys %}