Skip nodes functionality for Package and Network modules

Added to main entrypoint
- Skip nodes using simple argument with '*' as a trailing wildcard
- Skip nodes using file list

Usability improovement
- Node list preview in status line
- Node stats alignment in net report

Minor fixes:
- Python version detection (3.5+)
- Node counter for each status
- Proper node skip handling

Change-Id: I086ef501bc06f0e739df25349257f1c63a2e2fcf
Related-PROD: PROD-35009
diff --git a/cfg_checker/cfg_check.py b/cfg_checker/cfg_check.py
index 5c986a2..2ce37eb 100644
--- a/cfg_checker/cfg_check.py
+++ b/cfg_checker/cfg_check.py
@@ -32,6 +32,20 @@
         action='store_true', default=True,
         help="Use sudo for getting salt creds"
     )
+
+    parser.add_argument(
+        '--skip-nodes',
+        metavar='skip_string', default=None,
+        help="String with nodes to skip. Only trailing '*' supported!"
+             " Example: 'cmp*,ctl01'"
+    )
+
+    parser.add_argument(
+        '--skip-nodes-file',
+        metavar='skip_nodes_file', default=None,
+        help="Filename with nodes to skip. Note: use fqdn node names."
+    )
+
     subparsers = parser.add_subparsers(dest='command')
 
     # create parsers
diff --git a/cfg_checker/common/const.py b/cfg_checker/common/const.py
index 685c79a..3b17099 100644
--- a/cfg_checker/common/const.py
+++ b/cfg_checker/common/const.py
@@ -8,6 +8,7 @@
 _cnt = itertools.count()
 NODE_DOWN = next(_cnt)
 NODE_UP = next(_cnt)
+NODE_SKIP = next(_cnt)
 
 # version const order is important!
 # biggest get shown in report top row
@@ -42,12 +43,13 @@
     VERSION_DOWN: "downgraded",
     VERSION_WARN: "warning",
     VERSION_ERR: "error",
-    VERSION_NA: "no status"
+    VERSION_NA: "nostatus"
 }
 
 node_status = {
     NODE_UP: "up",
-    NODE_DOWN: "down"
+    NODE_DOWN: "down",
+    NODE_SKIP: "skip"
 }
 
 uknown_code = "unk"
diff --git a/cfg_checker/common/other.py b/cfg_checker/common/other.py
index a385b90..5a4c552 100644
--- a/cfg_checker/common/other.py
+++ b/cfg_checker/common/other.py
@@ -126,20 +126,20 @@
         else:
             raise ConfigException(_message)
 
-    def get_nodes_list(self, env, nodes_list):
+    def get_nodes_list(self, nodes_list, env_sting=None):
         _list = []
-        if env is None:
+        if env_sting is None:
             # nothing supplied, use the one in repo
             try:
                 if not nodes_list:
                     return []
-                with open(os.path.join(pkg_dir, nodes_list)) as _f:
+                with open(nodes_list) as _f:
                     _list.extend(_f.read().splitlines())
             except IOError as e:
                 raise ConfigException("# Error while loading file, '{}': "
                                       "{}".format(e.filename, e.strerror))
         else:
-            _list.extend(self.node_string_to_list(env))
+            _list.extend(self.node_string_to_list(env_sting))
 
         # validate names
         _invalid = []
@@ -151,7 +151,7 @@
             else:
                 _valid.append(_name)
 
-        return _valid
+        return _valid, _invalid
 
 
 utils = Utils()
diff --git a/cfg_checker/common/settings.py b/cfg_checker/common/settings.py
index 92c17a5..cca5142 100644
--- a/cfg_checker/common/settings.py
+++ b/cfg_checker/common/settings.py
@@ -1,4 +1,5 @@
 import os
+import sys
 
 from cfg_checker.common.exception import ConfigException
 
@@ -15,11 +16,17 @@
 
 
 class CheckerConfiguration(object):
+    @staticmethod
     def load_nodes_list():
-        return utils.get_nodes_list(
-            os.environ.get('CFG_ALL_NODES', None),
-            os.environ.get('SALT_NODE_LIST_FILE', None)
-        )
+        _file = os.environ.get('SALT_NODE_LIST_FILE', None)
+        if _file:
+            _v, _ = utils.get_nodes_list(
+                os.path.join(pkg_dir, _file),
+                env_sting=os.environ.get('CFG_ALL_NODES', None)
+            )
+            return _v
+        else:
+            return None
 
     def _init_values(self):
         """Load values from environment variables or put default ones
@@ -119,6 +126,16 @@
     def __init__(self):
         """Base configuration class. Only values that are common for all scripts
         """
+        # Make sure we running on Python 3
+        if sys.version_info[0] < 3 and sys.version_info[1] < 5:
+            logger_cli.error("# ERROR: Python 3.5+ is required")
+            sys.exit(1)
+        else:
+            logger_cli.debug("### Python version is {}.{}".format(
+                sys.version_info[0],
+                sys.version_info[1]
+            ))
+
         _env = os.getenv('SALT_ENV', None)
         self._init_env(_env)
         self._init_values()
diff --git a/cfg_checker/helpers/args_utils.py b/cfg_checker/helpers/args_utils.py
index 726c20e..498ed30 100644
--- a/cfg_checker/helpers/args_utils.py
+++ b/cfg_checker/helpers/args_utils.py
@@ -11,6 +11,20 @@
         self.print_help()
 
 
+def get_skip_args(args):
+    if hasattr(args, "skip_nodes"):
+        _skip = getattr(args, "skip_nodes")
+        if _skip:
+            _skip = _skip.split(',')
+    else:
+        _skip = None
+    if hasattr(args, "skip_nodes_file"):
+        _skip_file = getattr(args, "skip_nodes_file")
+    else:
+        _skip_file = None
+    return _skip, _skip_file
+
+
 def get_arg(args, str_arg):
     _attr = getattr(args, str_arg)
     if _attr:
diff --git a/cfg_checker/modules/network/__init__.py b/cfg_checker/modules/network/__init__.py
index 71df82d..28d08c4 100644
--- a/cfg_checker/modules/network/__init__.py
+++ b/cfg_checker/modules/network/__init__.py
@@ -71,7 +71,11 @@
     # should not print map, etc...
     # Just bare summary and errors
     logger_cli.info("# Network check to console")
-    netChecker = checker.NetworkChecker()
+    _skip, _skip_file = args_utils.get_skip_args(args)
+    netChecker = checker.NetworkChecker(
+        skip_list=_skip,
+        skip_list_file=_skip_file
+    )
     netChecker.check_networks()
 
     # save what was collected
@@ -93,8 +97,11 @@
     logger_cli.info("# Network report (check, node map")
 
     _filename = args_utils.get_arg(args, 'html')
-
-    netChecker = checker.NetworkChecker()
+    _skip, _skip_file = args_utils.get_skip_args(args)
+    netChecker = checker.NetworkChecker(
+        skip_list=_skip,
+        skip_list_file=_skip_file
+    )
     netChecker.check_networks(map=False)
 
     # save what was collected
@@ -108,8 +115,11 @@
     # Network Map
     # Should generate network map to console or HTML
     logger_cli.info("# Network report")
-
-    networkMap = mapper.NetworkMapper()
+    _skip, _skip_file = args_utils.get_skip_args(args)
+    networkMap = mapper.NetworkMapper(
+        skip_list=_skip,
+        skip_list_file=_skip_file
+    )
     networkMap.prepare_all_maps()
     networkMap.create_map()
     networkMap.print_map()
@@ -120,7 +130,11 @@
 def do_list(args):
     # Network List
     # Should generate network map to console or HTML
-    _map = mapper.NetworkMapper()
+    _skip, _skip_file = args_utils.get_skip_args(args)
+    _map = mapper.NetworkMapper(
+        skip_list=_skip,
+        skip_list_file=_skip_file
+    )
     reclass = _map.map_network(_map.RECLASS)
     runtime = _map.map_network(_map.RUNTIME)
 
@@ -141,7 +155,13 @@
     if not args.cidr:
         logger_cli.error("\n# Use mcp-check network list to get list of CIDRs")
     _cidr = args_utils.get_arg(args, "cidr")
-    _pinger = pinger.NetworkPinger(mtu=args.mtu, detailed=args.detailed)
+    _skip, _skip_file = args_utils.get_skip_args(args)
+    _pinger = pinger.NetworkPinger(
+        mtu=args.mtu,
+        detailed=args.detailed,
+        skip_list=_skip,
+        skip_list_file=_skip_file
+    )
 
     _ret = _pinger.ping_nodes(_cidr)
 
diff --git a/cfg_checker/modules/network/checker.py b/cfg_checker/modules/network/checker.py
index acd3bb1..c590d13 100644
--- a/cfg_checker/modules/network/checker.py
+++ b/cfg_checker/modules/network/checker.py
@@ -5,10 +5,18 @@
 
 
 class NetworkChecker(object):
-    def __init__(self):
+    def __init__(
+        self,
+        skip_list=None,
+        skip_list_file=None
+    ):
         logger_cli.debug("... init error logs folder")
         self.errors = NetworkErrors()
-        self.mapper = NetworkMapper(self.errors)
+        self.mapper = NetworkMapper(
+            self.errors,
+            skip_list=skip_list,
+            skip_list_file=skip_list_file
+        )
 
     def check_networks(self, map=True):
         self.mapper.map_network(self.mapper.RECLASS)
@@ -41,6 +49,7 @@
             filename
         )
         _report({
+            "domain": self.mapper.domain,
             "nodes": self.mapper.nodes,
             "map": self.mapper.map,
             "mcp_release": self.mapper.cluster['mcp_release'],
diff --git a/cfg_checker/modules/network/mapper.py b/cfg_checker/modules/network/mapper.py
index 483c11f..51f52bb 100644
--- a/cfg_checker/modules/network/mapper.py
+++ b/cfg_checker/modules/network/mapper.py
@@ -31,12 +31,21 @@
     CONFIG = "config"
     RUNTIME = "runtime"
 
-    def __init__(self, errors_class=None):
+    def __init__(
+        self,
+        errors_class=None,
+        skip_list=None,
+        skip_list_file=None
+    ):
         logger_cli.info("# Initializing mapper")
         # init networks and nodes
         self.networks = {}
-        self.nodes = salt_master.get_nodes()
+        self.nodes = salt_master.get_nodes(
+            skip_list=skip_list,
+            skip_list_file=skip_list_file
+        )
         self.cluster = salt_master.get_info()
+        self.domain = salt_master.domain
         # init and pre-populate interfaces
         self.interfaces = {k: {} for k in self.nodes}
         # Init errors class
diff --git a/cfg_checker/modules/network/pinger.py b/cfg_checker/modules/network/pinger.py
index 0500284..5b12a94 100644
--- a/cfg_checker/modules/network/pinger.py
+++ b/cfg_checker/modules/network/pinger.py
@@ -10,10 +10,21 @@
 
 # This is independent class with a salt.nodes input
 class NetworkPinger(object):
-    def __init__(self, mtu=None, detailed=False, errors_class=None):
+    def __init__(
+        self,
+        mtu=None,
+        detailed=False,
+        errors_class=None,
+        skip_list=None,
+        skip_list_file=None
+    ):
         logger_cli.info("# Initializing")
         # all active nodes in the cloud
-        self.target_nodes = salt_master.get_nodes()
+        self.target_nodes = salt_master.get_nodes(
+            skip_list=skip_list,
+            skip_list_file=skip_list_file
+        )
+
         # default MTU value
         self.target_mtu = mtu if mtu else 64
         # only data
diff --git a/cfg_checker/modules/packages/__init__.py b/cfg_checker/modules/packages/__init__.py
index 41dfca1..2d0cc79 100644
--- a/cfg_checker/modules/packages/__init__.py
+++ b/cfg_checker/modules/packages/__init__.py
@@ -112,9 +112,12 @@
         _kw = [args.exclude_keywords]
 
     # init connection to salt and collect minion data
+    _skip, _skip_file = args_utils.get_skip_args(args)
     pChecker = checker.CloudPackageChecker(
         force_tag=args.force_tag,
-        exclude_keywords=_kw
+        exclude_keywords=_kw,
+        skip_list=_skip,
+        skip_list_file=_skip_file
     )
     # collect data on installed packages
     pChecker.collect_installed_packages()
diff --git a/cfg_checker/modules/packages/checker.py b/cfg_checker/modules/packages/checker.py
index 8f30f3c..fb02db2 100644
--- a/cfg_checker/modules/packages/checker.py
+++ b/cfg_checker/modules/packages/checker.py
@@ -12,10 +12,19 @@
 
 
 class CloudPackageChecker(object):
-    def __init__(self, force_tag=None, exclude_keywords=[]):
+    def __init__(
+        self,
+        force_tag=None,
+        exclude_keywords=[],
+        skip_list=None,
+        skip_list_file=None
+    ):
         # Init salt master info
         if not salt_master.nodes:
-            salt_master.nodes = salt_master.get_nodes()
+            salt_master.nodes = salt_master.get_nodes(
+                skip_list=skip_list,
+                skip_list_file=skip_list_file
+            )
 
         # check that this env tag is present in Manager
         self.rm = RepoManager()
@@ -57,6 +66,7 @@
         _data['status_err'] = const.VERSION_ERR
         _data['status_warn'] = const.VERSION_WARN
         _data['status_down'] = const.VERSION_DOWN
+        _data['status_skip'] = const.VERSION_NA
 
         # Presort packages
         _data['critical'] = {}
diff --git a/cfg_checker/modules/reclass/comparer.py b/cfg_checker/modules/reclass/comparer.py
index 47e4baf..3c308e0 100644
--- a/cfg_checker/modules/reclass/comparer.py
+++ b/cfg_checker/modules/reclass/comparer.py
@@ -151,11 +151,15 @@
             # ignore _source key
             if k == "_source":
                 continue
-            # ignore secrets
+            # ignore secrets and other secure stuff
             if isinstance(k, str) and k == "secrets.yml":
                 continue
             if isinstance(k, str) and k.find("_password") > 0:
                 continue
+            if isinstance(k, str) and k.find("_key") > 0:
+                continue
+            if isinstance(k, str) and k.find("_token") > 0:
+                continue
             # check if this is an env name cluster entry
             if dict2 is not None and \
                     k == self.model_name_1 and \
diff --git a/cfg_checker/nodes.py b/cfg_checker/nodes.py
index c261752..13ab3e7 100644
--- a/cfg_checker/nodes.py
+++ b/cfg_checker/nodes.py
@@ -3,7 +3,9 @@
 from copy import deepcopy
 
 from cfg_checker.clients import get_salt_remote, salt
-from cfg_checker.common import config, const
+from cfg_checker.common import config
+from cfg_checker.common.const import all_roles_map
+from cfg_checker.common.const import NODE_UP, NODE_DOWN, NODE_SKIP
 from cfg_checker.common import logger, logger_cli
 from cfg_checker.common import utils
 from cfg_checker.common.exception import SaltException
@@ -12,7 +14,7 @@
 node_tmpl = {
     'role': '',
     'node_group': '',
-    'status': const.NODE_DOWN,
+    'status': NODE_DOWN,
     'pillars': {},
     'grains': {}
 }
@@ -25,7 +27,7 @@
         self.salt = salt
         self.nodes = None
 
-    def gather_node_info(self):
+    def gather_node_info(self, skip_list, skip_list_file):
         # Keys for all nodes
         # this is not working in scope of 2016.8.3, will overide with list
         logger_cli.debug("... collecting node names existing in the cloud")
@@ -61,33 +63,86 @@
         else:
             _minions = self.node_keys['minions']
 
+        # Skip nodes if needed
+        _skipped_minions = []
+        # skip list file
+        if skip_list_file:
+            _valid, _invalid = utils.get_nodes_list(skip_list_file)
+            logger_cli.info(
+                "\n# WARNING: Detected invalid entries "
+                "in nodes skip list:\n".format(
+                    "\n".join(_invalid)
+                )
+            )
+            _skipped_minions.extend(_valid)
+        # process wildcard, create node list out of mask
+        if skip_list:
+            _list = []
+            _invalid = []
+            for _item in skip_list:
+                if '*' in _item:
+                    _str = _item[:_item.index('*')]
+                    _nodes = [_m for _m in _minions if _m.startswith(_str)]
+                    if not _nodes:
+                        logger_cli.warn(
+                            "# WARNING: No nodes found for {}".format(_item)
+                        )
+                    _list.extend(_nodes)
+                else:
+                    if _item in _minions:
+                        _list += _item
+                    else:
+                        logger_cli.warn(
+                            "# WARNING: No node found for {}".format(_item)
+                        )
+            # removing duplicates
+            _list = list(set(_list))
+            _skipped_minions.extend(_list)
+
         # in case API not listed minions, we need all that answer ping
         _active = self.salt.get_active_nodes()
         logger_cli.info("-> nodes responded: {}".format(len(_active)))
         # iterate through all accepted nodes and create a dict for it
         self.nodes = {}
         self.skip_list = []
+        _domains = set()
         for _name in _minions:
             _nc = utils.get_node_code(_name)
-            _rmap = const.all_roles_map
+            _rmap = all_roles_map
             _role = _rmap[_nc] if _nc in _rmap else 'unknown'
-            _status = const.NODE_UP if _name in _active else const.NODE_DOWN
-            if _status == const.NODE_DOWN:
+            if _name in _skipped_minions:
+                _status = NODE_SKIP
                 self.skip_list.append(_name)
-                logger_cli.info("-> '{}' is down, marked to skip".format(
-                    _name
-                ))
+            else:
+                _status = NODE_UP if _name in _active else NODE_DOWN
+                if _status == NODE_DOWN:
+                    self.skip_list.append(_name)
+                    logger_cli.info(
+                        "-> '{}' is down, "
+                        "added to skip list".format(
+                            _name
+                        )
+                    )
             self.nodes[_name] = deepcopy(node_tmpl)
+            self.nodes[_name]['shortname'] = _name.split(".", 1)[0]
+            _domains.add(_name.split(".", 1)[1])
             self.nodes[_name]['node_group'] = _nc
             self.nodes[_name]['role'] = _role
             self.nodes[_name]['status'] = _status
+        _domains = list(_domains)
+        if len(_domains) > 1:
+            logger_cli.warning(
+                "Multiple domains detected: {}".format(",".join(_domains))
+            )
+        else:
+            self.domain = _domains[0]
         logger_cli.info("-> {} nodes inactive".format(len(self.skip_list)))
         logger_cli.info("-> {} nodes collected".format(len(self.nodes)))
 
         # form an all nodes compound string to use in salt
         self.active_nodes_compound = self.salt.compound_string_from_list(
             filter(
-                lambda nd: self.nodes[nd]['status'] == const.NODE_UP,
+                lambda nd: self.nodes[nd]['status'] == NODE_UP,
                 self.nodes
             )
         )
@@ -96,7 +151,7 @@
         #     lambda nd: self.nodes[nd]['role'] == const.all_roles_map['cfg'],
         #     self.nodes
         # )
-        _role = const.all_roles_map['cfg']
+        _role = all_roles_map['cfg']
         _filtered = [n for n, v in self.nodes.items() if v['role'] == _role]
         if len(_filtered) < 1:
             raise SaltException(
@@ -137,9 +192,12 @@
         else:
             return False
 
-    def get_nodes(self):
+    def get_nodes(self, skip_list=None, skip_list_file=None):
         if not self.nodes:
-            self.gather_node_info()
+            if not skip_list and config.skip_nodes:
+                self.gather_node_info(config.skip_nodes, skip_list_file)
+            else:
+                self.gather_node_info(skip_list, skip_list_file)
         return self.nodes
 
     def get_info(self):
@@ -177,7 +235,7 @@
             if target_key not in data:
                 data[target_key] = None
             # Save data
-            if data['status'] == const.NODE_DOWN:
+            if data['status'] in [NODE_DOWN, NODE_SKIP]:
                 data[target_key] = None
             elif node not in _result:
                 continue
@@ -219,7 +277,7 @@
                 if _key not in _data:
                     _data[_key] = {}
                 _data = _data[_key]
-            if data['status'] == const.NODE_DOWN:
+            if data['status'] in [NODE_DOWN, NODE_SKIP]:
                 _data[_pillar_keys[-1]] = None
             elif not _result[node]:
                 logger_cli.debug(
diff --git a/cfg_checker/reports/reporter.py b/cfg_checker/reports/reporter.py
index 3d3ede3..e0a746e 100644
--- a/cfg_checker/reports/reporter.py
+++ b/cfg_checker/reports/reporter.py
@@ -25,6 +25,7 @@
 
 UP = const.NODE_UP
 DOWN = const.NODE_DOWN
+SKIP = const.NODE_SKIP
 
 
 def line_breaks(text):
@@ -265,7 +266,7 @@
             # parse them and put into dict
             for node, dt in _dict.items():
                 dt[_key] = {}
-                if dt['status'] == DOWN:
+                if dt['status'] == DOWN or dt['status'] == SKIP:
                     continue
                 if not dt[_key_r]:
                     # no stats collected, put negatives
@@ -295,7 +296,7 @@
             # parse them and put into dict
             for node, dt in _dict.items():
                 dt[_key] = {}
-                if dt['status'] == DOWN:
+                if dt['status'] == DOWN or dt['status'] == SKIP:
                     continue
                 if not dt[_key_r]:
                     # no stats collected, put negatives
@@ -327,7 +328,7 @@
             _f_cmd(_cmd, _key_r, target_dict=_dict)
             for node, dt in _dict.items():
                 dt[_key] = {}
-                if dt['status'] == DOWN:
+                if dt['status'] == DOWN or dt['status'] == SKIP:
                     continue
                 if not dt[_key_r]:
                     # no stats collected, put negatives
@@ -359,7 +360,7 @@
             for node in _kvm:
                 dt = _dict[node]
                 dt[_key] = {}
-                if dt['status'] == DOWN:
+                if dt['status'] == DOWN or dt['status'] == SKIP:
                     continue
                 if not dt[_key_r]:
                     # no stats collected, put negatives
@@ -394,7 +395,7 @@
                 # totals for start mark
                 _ts = [0, 0, 0, 0]
                 # skip if node is down
-                if dt['status'] == DOWN:
+                if dt['status'] == DOWN or dt['status'] == SKIP:
                     dt[_key] = {
                         "total": [-1, -1, -1, -1]
                     }
@@ -492,6 +493,10 @@
                 dt["disk"]["unknown"] = {}
                 dt["disk_max_dev"] = "unknown"
                 continue
+            if dt['status'] == SKIP:
+                dt["disk"]["skipped"] = {}
+                dt["disk_max_dev"] = "skipped"
+                continue
             if not dt[_key_r]:
                 # no stats collected, put negatives
                 dt.pop(_key_r)
diff --git a/templates/network_check_tmpl.j2 b/templates/network_check_tmpl.j2
index 6cb3e89..4057a22 100644
--- a/templates/network_check_tmpl.j2
+++ b/templates/network_check_tmpl.j2
@@ -50,21 +50,21 @@
         }
         td > .disk_group {
             display: grid;
-            grid-template-columns: auto auto auto auto auto;
+            grid-template-columns: 170px 40px 40px 40px 40px;
             padding-left: 0px;
             padding-right: 0px;
             margin: 1px;
         }
-        td > .ram_group, td > .net_group {
+        td > .ram_group {
             display: grid;
-            grid-template-columns: auto auto auto auto;
+            grid-template-columns: 40px 40px 40px 40px;
             padding-left: 0px;
             padding-right: 0px;
             margin: 1px;
         }
         td > .net_group {
             display: grid;
-            grid-template-columns: auto auto auto auto auto;
+            grid-template-columns: 50px 50px 40px 40px 40px;
             padding-left: 0px;
             padding-right: 0px;
             margin: 1px;
@@ -87,20 +87,42 @@
 
 	    .status_none { border-radius: 10px; width: 8px; }
 	    .status_up { border-radius: 10px; width: 8px; background-color: #393; color: #393; }
-	    .status_down { border-radius: 10px; width: 8px; background-color: #933; color: #933; }
-        
+        .status_down { border-radius: 10px; width: 8px; background-color: #933; color: #933; }
+        .status_skip { border-radius: 10px; width: 8px; background-color: #aaa; color: #aaa; }
+
+        .down > .col_name, .down > .col_role, .down > .col_down {
+            background-color: #fee;
+            background-image: linear-gradient(white, #fee, #fee, white);
+            margin: 0px;
+        }
+        .skip > .col_name, .skip > .col_role, .skip > .col_skip {
+            background-color: #eee;
+            background-image: linear-gradient(white, #eee, #eee, white);
+            margin: 0px;
+        }
+        .down > .col_name, .skip > .col_name { padding-left: 3px; }
+        .down > .col_role, .skip > .col_role { padding-left: 5px; }
+
         .head { height: 18px; }
-	    .col_name {	width: 250px; }
+	    .col_name {	width: 90px; }
 	    .col_role {	width: 130px; }
 	    .col_vendor { width: 70px; }
         .col_release { width: 100px; }
         .col_kernel { min-width: 100px; }
         .col_vcpu { min-width: 40px; }
         .col_net { min-width: 150px; }
-        .col_ram { min-width: 150px; }
-        .col_disk { min-width: 200px; }
+        .col_ram { min-width: 162px; }
+        .col_disk { min-width: 290px; }
+
+        .col_down, .col_skip {
+            min-width: 1020px;
+            text-align: left;
+            font-style: italic;
+            padding-left: 50px !important;
+            color: gray;
+        }
         
-        .col_node_notes { width: 384px; }
+        .col_node_notes { width: 225px; }
         .col_cpu_notes { width: 177px; }
 
         .meters {
@@ -137,9 +159,11 @@
         }
         .cpu { border-color: #a0c0a0; background-color: rgb(252, 248, 248); }
         .net { border-color: #c0c0a0; background-color: rgb(255, 255, 251); text-align: right; }
-        .ram { border-color: #a0c0c0; background-color: rgb(255, 250, 250); }
-        .disk { border-color: #cedfdf; background-color: rgb(237, 241, 243); }
-
+        .ram { border-color: #a0c0c0; background-color: rgb(255, 250, 250); text-align: right; }
+        .disk { border-color: #cedfdf; background-color: rgb(237, 241, 243); text-align: right; }
+        .disk_group > .path {
+            text-align: left;
+        }
         .map_grid {
             display: grid;
             grid-template-columns: auto auto auto auto auto auto auto auto auto auto;
@@ -262,36 +286,53 @@
                 </div>
             </td>
             <td class="head col_net">
-                <div class="meters net">
-                    <div class="meter net">Total</div>
-                    <div class="meter net">Dropped</div>
-                    <div class="meter net">Squeeze</div>
-                    <div class="meter net">Collide</div>
+                <div class="net_group">
+                    <div class="item net">vCpu</div>
+                    <div class="item net">Total</div>
+                    <div class="item net">Drop</div>
+                    <div class="item net">Sq.</div>
+                    <div class="item net">Cl.</div>
                 </div>
             </td>
             <td class="head col_ram">
-                <div class="meters">
-                    <div class="meter ram">Total</div>
-                    <div class="meter ram">Used</div>
-                    <div class="meter ram">Free</div>
-                    <div class="meter ram">Available</div>
+                <div class="ram_group">
+                    <div class="item ram">Total</div>
+                    <div class="item ram">Used</div>
+                    <div class="item ram">Free</div>
+                    <div class="item ram">Avail.</div>
                 </div>
             </td>
             <td class="head col_disk">
-                <div class="meters">
-                    <div class="meter disk">device</div>
-                    <div class="meter disk">total</div>
-                    <div class="meter disk">used</div>
-                    <div class="meter disk">free</div>
-                    <div class="meter disk">percent</div>
+                <div class="disk_group">
+                    <div class="item disk path">Device path</div>
+                    <div class="item disk">Total</div>
+                    <div class="item disk">Used</div>
+                    <div class="item disk">Free</div>
+                    <div class="item disk">%</div>
                 </div>
             </td>
         </tr>
     {% for node in nodes.keys() | sort %}
         {% set _ndat = nodes[node] %}
+        {% set _status = _ndat['status'] | node_status_class %}
+        {% if _status == 'down' %}
+        <tr class="node down">
+            <td class="status_{{ _status }}">.</td>
+            <td class="head col_name">{{ _ndat['shortname'] }}</td>
+            <td class="head col_role">{{ _ndat['role'] }}</td>
+            <td class="head col_down" colspan="7"> ...no data collected: node is down</td>
+        </tr>
+        {% elif _status == 'skip' %}
+        <tr class="node skip">
+            <td class="status_{{ _status }}">.</td>
+            <td class="head col_name">{{ _ndat['shortname'] }}</td>
+            <td class="head col_role">{{ _ndat['role'] }}</td>
+            <td class="head col_skip" colspan="7"> ...no skipped from processing</td>
+        </tr>
+        {% else %}
         <tr class="node" onclick="toggleClassByID('info_{{ node }}')" id='info_{{ node }}_button'>
             <td class="status_{{ _ndat['status'] | node_status_class }}">.</td>
-            <td class="head col_name">{{ node }}</td>
+            <td class="head col_name">{{ _ndat['shortname'] }}</td>
             <td class="head col_role">{{ _ndat['role'] }}</td>
             <td class="head col_vendor">{{ _ndat['node_type'] }}</td>
             <td class="head col_release">{{ _ndat['linux_arch'] }}/{{ _ndat['linux_codename'] }}</td>
@@ -303,6 +344,7 @@
             </td>
             <td class="head col_net">
                 <div class="net_group">
+                    <div class="item net">All</div>
                     <div class="item net">{{ _ndat['net_stats']['total'][0] }}</div>
                     <div class="item net">{{ _ndat['net_stats']['total'][1] }}</div>
                     <div class="item net">{{ _ndat['net_stats']['total'][2] }}</div>
@@ -319,7 +361,7 @@
             </td>
             <td class="head col_disk">
                 <div class="disk_group">
-                    <div class="item disk">{{ _ndat['disk_max_dev'] }}</div>
+                    <div class="item disk path">{{ _ndat['disk_max_dev'] }}</div>
                     {% for val in _ndat['disk'][_ndat['disk_max_dev']]['v'] %}
                     <div class="item disk {{ _ndat['disk'][_ndat['disk_max_dev']]['f'] }}">{{ val }}</div>
                     {% endfor %}
@@ -359,7 +401,7 @@
             <td class="col_disk">
                 <div class="disk_group">
                     {% for dev in _ndat['disk'].keys() | sort %}
-                    <div class="item disk">{{ dev }}</div>
+                    <div class="item disk path">{{ dev }}</div>
                     {% for val in _ndat['disk'][dev]['v'] %}
                     <div class="item disk {{ _ndat['disk'][dev]['f'] }}">{{ val }}</div>
                     {% endfor %}
@@ -367,6 +409,7 @@
                 </div>
             </td>
         </tr>
+        {% endif %}
     {% endfor %}
     </table>
     <hr>
@@ -453,7 +496,7 @@
 
 <!-- Cluster nodes page -->
 {% call nodes_page(nodes, "nodes") %}
-    Cluster nodes status and simple meterings
+    Cluster nodes status and simple meterings in *.{{ domain }}
 {% endcall %}
 
 <!-- Cluster nodes page -->
diff --git a/templates/pkg_versions_html.j2 b/templates/pkg_versions_html.j2
index dea3956..85a5ee2 100644
--- a/templates/pkg_versions_html.j2
+++ b/templates/pkg_versions_html.j2
@@ -20,12 +20,11 @@
         td.package_name {column-width: 200px; padding-left: 10px; text-align: left;}
         td.node_name {column-width: 210px;}
         td.installed {column-width: 16%;}
-        td.status_container {column-width: 200px;}
         td.candidate {column-width: 16%;}
         td.release {column-width: 13%;}
 
         .status_container {
-            display: inline-block;
+            display: inline-flex;
         }
 
         .status {
@@ -51,6 +50,22 @@
             text-align: center;
         }
 
+        .node {
+            display: block;
+            float: left;
+            width: auto;
+            padding: 1px;
+            padding-left: 2px;
+            padding-right: 2px;
+            margin-left: 3px;
+            border-width: 1px;
+            border-color: gray;
+            border-style: solid;
+            color: gray;
+            background-color: white;
+            text-align: center;
+        }
+
         .status_container .ok {
             color: white;
             background-color: #113b11;
@@ -88,6 +103,25 @@
             background-color: #d4dad9;
         }
 
+        .status_container .node_error {
+            color: red;
+            border-color: red;
+            background-color: white;
+        }
+
+        .status_container .node_warning {
+            color: olive;
+            border-color: olive;
+            background-color: white;
+        }
+
+        .status_container .node_more {
+            color: gray;
+            border-color: gray;
+            background-color: white;
+            border-style: none;
+        }
+
         .status_container .text {
             width: 500px;
             font-size: 1em;
@@ -167,18 +201,36 @@
                 {% if action_shown | pkg_action_label %}
                     <div class="action {{ action_shown | pkg_action_class }}">{{ action_shown | pkg_action_label }}</div>
                 {% endif %}
+                {% set counter = [0] %}
+                {% for status in dat['results'].keys() | sort(reverse=true) %}
+                {% if status_err == status or status_warn == status %}
+                    {% for action in dat['results'][status].keys() | sort(reverse=true) %}
+                    {% for node in dat['results'][status][action].keys() | sort %}
+                    {% if counter.append(counter.pop() + 1) %}{% endif %}
+                    {% if counter[0] < 10 %}
+                        <div class="node node_{{ status | pkg_status_class }}" alt="{{ counter }}">{{ nodes[node].shortname }}</div>
+                    {% endif %}
+                    {% endfor %}
+                    {% endfor %}
+                {% endif %}
+                {% endfor %}
+
+                {% if counter[0] > 10 %}
+                    <div class="node node_more" alt="{{ counter[0]-10 }} more">...</div>
+                {% endif %}
+
             </td>
         </tr>
         <tr class="collapsable" id="{{ id_label }}_{{ pkg_name }}_{{ status_shown }}_{{ action_shown }}"><td colspan="7">
         <table class="nodes"><tbody>
+        {% set counter = [0] %}
         {% for status in dat['results'].keys() | sort(reverse=true) %}
         {% for action in dat['results'][status].keys() | sort(reverse=true) %}
-        {% set counter = 1 + loop.index0 %}
         {% for node in dat['results'][status][action].keys() | sort %}
-        {% set n_counter = 1 + loop.index0 %}
+        {% if counter.append(counter.pop() + 1) %}{% endif %}
         {% set nd = dat['results'][status][action][node] %}
         <tr>
-            <td class="repo">{{ n_counter }}</td>
+            <td class="repo">{{ counter[0] }}</td>
             <td class="node_name">{{ node }}</td>
             <td class="status_container">
                 <div class="status {{ status | pkg_status_class }}">{{ status | pkg_status_label }}</div>