Additions and fixes to network check

- Per interface tree maps
- proper virtial nodes detection
- KVM nodes listing
- CPU count fix
- Basic service fail check (wip)

Change-Id: I62b68793404eeff957ef70468c954df2fda869a5
Related-PROD: PROD-38972
diff --git a/cfg_checker/common/file_utils.py b/cfg_checker/common/file_utils.py
index 9c043a8..c550184 100644
--- a/cfg_checker/common/file_utils.py
+++ b/cfg_checker/common/file_utils.py
@@ -48,9 +48,9 @@
 
 def read_file_as_lines(filename):
     _list = []
-    with open(filename, 'r') as fr:
+    with open(filename, 'rt') as fr:
         for line in fr:
-            _list.append(line)
+            _list.append(line.rstrip())
     return _list
 
 
diff --git a/cfg_checker/modules/network/mapper.py b/cfg_checker/modules/network/mapper.py
index ba9a256..cea81bf 100644
--- a/cfg_checker/modules/network/mapper.py
+++ b/cfg_checker/modules/network/mapper.py
@@ -243,6 +243,123 @@
                         # data is already there, just add VIP
                         net_data['ifs'].append(_if)
 
+            def process_interface(lvl, interface, tree, res):
+                # get childs for each root
+                # tree row item (<if_name>, [<parents>], [<childs>])
+                if lvl not in tree:
+                    # - no level - add it
+                    tree[lvl] = {}
+                # there is such interface in this level?
+                if interface not in tree[lvl]:
+                    # - IF not present
+                    # -- get parents, add
+                    _p = res[interface]['lower']
+                    # if None, put empty list
+                    _p = _p if _p else []
+                    # -- get childs, add
+                    _c = res[interface]['upper']
+                    # if None, put empty list
+                    _c = _c if _c else []
+                    tree[lvl].update({
+                        interface: {
+                            "parents": _p,
+                            "children": _c,
+                            "size": len(_p) if len(_p) > len(_c) else len(_c)
+                        }
+                    })
+                    for p_if in tree[lvl][interface]["parents"]:
+                        # -- cycle: execute process for next parent, lvl-1
+                        process_interface(lvl-1, p_if, tree, res)
+                    for c_if in tree[lvl][interface]["children"]:
+                        # -- cycle: execute process for next child, lvl+1
+                        process_interface(lvl+1, c_if, tree, res)
+                else:
+                    # - IF present - exit (been here already)
+                    return
+
+            def _put(cNet, cIndex, _list):
+                for _cI in range(cIndex, len(_list)):
+                    # add child per index
+                    # if space is free
+                    if not _list[_cI]:
+                        _list[_cI] = cNet
+                        break
+
+            # build network hierachy
+            nr = node_data['networks']
+            # walk interface tree
+            for _ifname in node_data['networks']:
+                _tree = {}
+                _level = 0
+                process_interface(_level, _ifname, _tree, nr)
+                # save tree for node/if
+                node_data['networks'][_ifname]['tree'] = _tree
+
+                # debug, print built tree
+                # logger_cli.debug("# '{}'".format(_ifname))
+                lvls = _tree.keys()
+                lvls.sort()
+                n = len(lvls)
+                m = max([len(_tree[k].keys()) for k in _tree.keys()])
+                matrix = [["" for i in range(m)] for j in range(n)]
+                x = 0
+                while True:
+                    _lv = lvls.pop(0)
+                    # get all interfaces on this level
+                    nets = _tree[_lv].keys()
+                    while True:
+                        y = 0
+                        # get next interface
+                        _net = nets.pop(0)
+                        # all nets
+                        _a = [_net]
+                        # put current interface if this is only one left
+                        if not _tree[_lv][_net]['children']:
+                            if _net not in matrix[x]:
+                                _put(_net, y, matrix[x])
+                            y += 1
+                        else:
+                            # get all nets with same child
+                            for _c in _tree[_lv][_net]['children']:
+                                for _o_net in nets:
+                                    if _c in _tree[_lv][_o_net]['children']:
+                                        _a.append(_o_net)
+                                # flush collected nets
+                                for idx in range(len(_a)):
+                                    if _a[idx] in matrix[x]:
+                                        # there is such interface on this level
+                                        # get index
+                                        _nI = matrix[x].index(_a[idx])
+                                        _put(_c, _nI, matrix[x+1])
+                                    else:
+                                        # there is no such interface
+                                        # add it
+                                        for _nI in range(len(matrix[x])):
+                                            if not matrix[x][_nI]:
+                                                matrix[x][_nI] = _a[idx]
+                                                # also, put child
+                                                _put(_c, _nI, matrix[x+1])
+                                                break
+                                    # remove collected nets from processing
+                                    if _a[idx] in nets:
+                                        nets.remove(_a[idx])
+                                y += len(_a)
+                        if not nets:
+                            x += 1
+                            break
+                    if not lvls:
+                        break
+
+                lines = []
+                _columns = [len(max([i for i in li])) for li in matrix]
+                for idx_y in range(m):
+                    line = ""
+                    for idx_x in range(n):
+                        _fmt = "{" + ":{}".format(_columns[idx_x]) + "} "
+                        line += _fmt.format(matrix[idx_x][idx_y])
+                    lines.append(line)
+                node_data['networks'][_ifname]['matrix'] = matrix
+                node_data['networks'][_ifname]['lines'] = lines
         return _runtime
 
     def map_network(self, source):
@@ -282,7 +399,11 @@
                     self.errors.NET_NO_RUNTIME_NETWORK,
                     reclass_net=str(network)
                 )
-                logger_cli.info("    {:-^50}".format(" No runtime network "))
+                logger_cli.warn(
+                    "WARN: {}: {}".format(
+                        " No runtime network ", str(network)
+                    )
+                )
                 continue
             # hostnames
             names = sorted(_runtime[network].keys())
@@ -464,6 +585,8 @@
                             "interface": _if_name,
                             "interface_error": _if_rc,
                             "interface_note": _if_name_suffix,
+                            "interface_map": "\n".join(_host['lines']),
+                            "interface_matrix": _host['matrix'],
                             "ip_address": _ip_str,
                             "address_type": _proto,
                             "rt_mtu": _host['mtu'],
diff --git a/cfg_checker/modules/reclass/comparer.py b/cfg_checker/modules/reclass/comparer.py
index 6591d16..8ef8894 100644
--- a/cfg_checker/modules/reclass/comparer.py
+++ b/cfg_checker/modules/reclass/comparer.py
@@ -62,7 +62,7 @@
                 _size = f.tell()
             # TODO: do smth with the data
             if not _yaml:
-                logger_cli.warning("WARN: empty file '{}'".format(fname))
+                # logger.warning("WARN: empty file '{}'".format(fname))
                 _yaml = {}
             else:
                 logger.debug("...loaded YAML '{}' ({}b)".format(fname, _size))
@@ -150,6 +150,11 @@
             # ignore _source key
             if k == "_source":
                 continue
+            # ignore secrets
+            if isinstance(k, str) and k == "secrets.yml":
+                continue
+            if isinstance(k, str) and k.find("_password") > 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 ca4e261..0ca1e85 100644
--- a/cfg_checker/nodes.py
+++ b/cfg_checker/nodes.py
@@ -147,7 +147,7 @@
         }
         return _info
 
-    def get_cmd_for_nodes(self, cmd, target_key, target_dict=None):
+    def get_cmd_for_nodes(self, cmd, target_key, target_dict=None, nodes=None):
         """Function runs. cmd.run and parses result into place
         or into dict structure provided
 
@@ -160,8 +160,9 @@
             _nodes = target_dict
         else:
             _nodes = self.nodes
-        _result = self.execute_cmd_on_active_nodes(cmd)
+        _result = self.execute_cmd_on_active_nodes(cmd, nodes=nodes)
         for node, data in _nodes.iteritems():
+            
             if node in self.skip_list:
                 logger_cli.debug(
                     "... '{}' skipped while collecting '{}'".format(
@@ -176,6 +177,8 @@
             # Save data
             if data['status'] == const.NODE_DOWN:
                 data[target_key] = None
+            elif node not in _result:
+                continue
             elif not _result[node]:
                 logger_cli.debug(
                     "... '{}' not responded after '{}'".format(
@@ -369,11 +372,11 @@
         self.not_responded = [_n for _n in _r.keys() if not _r[_n]]
         return _r
 
-    def execute_cmd_on_active_nodes(self, cmd):
+    def execute_cmd_on_active_nodes(self, cmd, nodes=None):
         # execute cmd
         self.not_responded = []
         _r = self.salt.cmd(
-            self.active_nodes_compound,
+            nodes if nodes else self.active_nodes_compound,
             'cmd.run',
             param=cmd,
             expr_form="compound"
diff --git a/cfg_checker/reports/reporter.py b/cfg_checker/reports/reporter.py
index 8059fab..7aa376a 100644
--- a/cfg_checker/reports/reporter.py
+++ b/cfg_checker/reports/reporter.py
@@ -1,9 +1,11 @@
 import abc
 import os
+import re
 import time
 
 from cfg_checker.common import const
 from cfg_checker.common import logger_cli
+from cfg_checker.common.file_utils import read_file_as_lines
 from cfg_checker.nodes import salt_master
 
 import jinja2
@@ -19,6 +21,10 @@
 _disk_critical = 90
 _ram_warn = 5
 _ram_critical = 3
+_softnet_interval = 5
+
+UP = const.NODE_UP
+DOWN = const.NODE_DOWN
 
 
 def line_breaks(text):
@@ -217,16 +223,61 @@
             else:
                 return int(value)
 
-        def _lscpu(field, key, _dict):
+        def _lscpu(_dict):
+            _key = "lscpu"
+            _key_r = "lscpu_raw"
+            # get all of the values
             _f_cmd = salt_master.get_cmd_for_nodes
-            _cmd = "lscpu | grep -e \"^{}:\" | cut -d\":\" -f2 " \
-                   "| sed -e 's/^[[:space:]]*//'"
-            _f_cmd(_cmd.format(field), key, target_dict=_dict)
+            _cmd = "lscpu | sed -n '/\\:/s/ \\+/ /gp'"
+            _f_cmd(_cmd, _key_r, target_dict=_dict)
+            # parse them and put into dict
+            for node, dt in _dict.iteritems():
+                dt[_key] = {}
+                if dt['status'] == DOWN:
+                    continue
+                lines = dt[_key_r].splitlines()
+                for line in lines:
+                    li = line.split(':')
+                    _var_name = li[0].lower()
+                    _var_name = re.sub(' ', '_', _var_name)
+                    _var_name = re.sub('|'.join(['\\(', '\\)']), '', _var_name)
+                    _var_value = li[1].strip()
+                    dt[_key][_var_name] = _var_value
+                dt.pop(_key_r)
+                # detect virtual nodes
+                if "hypervisor_vendor" in dt[_key]:
+                    dt['node_type'] = "virtual"
+                else:
+                    dt['node_type'] = "physical"
 
-        def _free(field, key, _dict):
+        def _free(_dict):
+            _key = "ram"
+            _key_r = "ram_raw"
             _f_cmd = salt_master.get_cmd_for_nodes
-            _cmd = "free -h | sed -n '/Mem/s/ \\+/ /gp' | cut -d\" \" -f {}"
-            _f_cmd(_cmd.format(field), key, target_dict=_dict)
+            _cmd = "free -h | sed -n '/Mem/s/ \\+/ /gp'"
+            _f_cmd(_cmd, _key_r, target_dict=_dict)
+            # parse them and put into dict
+            for node, dt in _dict.iteritems():
+                dt[_key] = {}
+                if dt['status'] == DOWN:
+                    continue
+                li = dt[_key_r].split()
+                dt[_key]['total'] = li[1]
+                dt[_key]['used'] = li[2]
+                dt[_key]['free'] = li[3]
+                dt[_key]['shared'] = li[4]
+                dt[_key]['cache'] = li[5]
+                dt[_key]['available'] = li[6]
+
+                _total = get_bytes(li[1])
+                _avail = get_bytes(li[6])
+                _m = _avail * 100.0 / _total
+                if _m < _ram_critical:
+                    dt[_key]["status"] = "fail"
+                elif _m < _ram_warn:
+                    dt[_key]["status"] = "warn"
+                else:
+                    dt[_key]["status"] = ""
 
         def _services(_dict):
             _key = "services"
@@ -236,6 +287,8 @@
             _f_cmd(_cmd, _key_r, target_dict=_dict)
             for node, dt in _dict.iteritems():
                 dt[_key] = {}
+                if dt['status'] == DOWN:
+                    continue
                 lines = dt[_key_r].splitlines()
                 for line in lines:
                     li = line.split()
@@ -249,11 +302,107 @@
                         dt[_key][_name] = None
                 dt.pop(_key_r)
 
+        def _vcp_status(_dict):
+            _key = "virsh"
+            _key_r = "virsh_raw"
+            salt_master.get_cmd_for_nodes(
+                "virsh list --all | sed -n -e '/[0-9]/s/ \\+/ /gp'",
+                _key_r,
+                target_dict=_dict,
+                nodes="kvm*"
+            )
+            _kvm = filter(lambda x: x.find("kvm") >= 0, _dict.keys())
+            for node in _kvm:
+                dt = _dict[node]
+                dt[_key] = {}
+                if dt['status'] == DOWN:
+                    continue
+                lines = dt[_key_r].splitlines()
+                for line in lines:
+                    li = line.split()
+                    _id = li[0]
+                    _name = li[1]
+                    _status = li[2]
+                    dt[_key][_name] = {
+                        'id': _id,
+                        'status': _status
+                    }
+                dt.pop(_key_r)
+
+        # query per-cpu and count totals
+        # total (0), dropped(1), squeezed (2), collision (7)
+        def _soft_net_stats(_dict):
+            _key = "net_stats"
+            _key_r = "net_stats_raw"
+            _f_cmd = salt_master.get_cmd_for_nodes
+            _cmd = "cat /proc/net/softnet_stat; echo \\#; " \
+                "sleep {}; cat /proc/net/softnet_stat".format(
+                    _softnet_interval
+                )
+            _f_cmd(_cmd, _key_r, target_dict=_dict)
+            for node, dt in _dict.iteritems():
+                _cpuindex = 1
+                _add_mode = True
+                # final totals
+                dt[_key] = {
+                    "total": [0, 0, 0, 0]
+                }
+                # totals for start mark
+                _ts = [0, 0, 0, 0]
+                # skip if node is down
+                if dt['status'] == DOWN:
+                    continue
+                lines = dt[_key_r].splitlines()
+                for line in lines:
+                    if line.startswith("#"):
+                        _add_mode = False
+                        _cpuindex = 1
+                        continue
+                    li = line.split()
+                    _c = [
+                        int(li[0], 16),
+                        int(li[1], 16),
+                        int(li[2], 16),
+                        int(li[7], 16)
+                    ]
+                    _id = "cpu{:02}".format(_cpuindex)
+                    if _id not in dt[_key]:
+                        dt[_key][_id] = []
+                    _dc = dt[_key][_id]
+                    if _add_mode:
+                        # saving values and adding totals
+                        dt[_key][_id] = _c
+                        # save start totals
+                        _ts = [_ts[i]+_c[i] for i in range(0, len(_c))]
+                    else:
+                        # this is second measurement
+                        # subtract all values
+                        for i in range(len(_c)):
+                            dt[_key][_id][i] = _c[i] - _dc[i]
+                            dt[_key]["total"][i] += _c[i]
+                    _cpuindex += 1
+                # finally, subtract initial totals
+                for k, v in dt[_key].iteritems():
+                    if k != "total":
+                        dt[_key][k] = [v[i] / 5. for i in range(len(v))]
+                    else:
+                        dt[_key][k] = [(v[i]-_ts[i])/5. for i in range(len(v))]
+                dt.pop(_key_r)
+
+        # prepare yellow and red marker values
         data["const"] = {
+            "net_interval": _softnet_interval,
             "ram_warn": _ram_warn,
             "ram_critical": _ram_critical,
             "disk_warn": _disk_warn,
-            "disk_critical": _disk_critical
+            "disk_critical": _disk_critical,
+            "services": read_file_as_lines(
+                            os.path.join(
+                                pkg_dir,
+                                'etc',
+                                'services.list'
+                            )
+                        )
         }
 
         # get kernel version
@@ -262,30 +411,12 @@
             "kernel",
             target_dict=data["nodes"]
         )
-        # cpu info
-        # Sample: VT-x, KVM, full
-        _lscpu("Virtualization", "virt_mode", data["nodes"])
-        _lscpu("Hypervisor vendor", "virt_vendor", data["nodes"])
-        _lscpu("Virtualization type", "virt_type", data["nodes"])
-        # sample: 4
-        _lscpu("CPU(s)", "cpus", data["nodes"])
+        # process lscpu data
+        _lscpu(data["nodes"])
 
         # free ram
         # sample: 16425392 14883144 220196
-        _free("2", "ram_total", data["nodes"])
-        _free("3", "ram_used", data["nodes"])
-        _free("4", "ram_free", data["nodes"])
-        _free("7", "ram_available", data["nodes"])
-        for _data in data["nodes"].itervalues():
-            _total = get_bytes(_data["ram_total"])
-            _avail = get_bytes(_data["ram_available"])
-            _m = _avail * 100.0 / _total
-            if _m < _ram_critical:
-                _data["ram_status"] = "fail"
-            elif _m < _ram_warn:
-                _data["ram_status"] = "warn"
-            else:
-                _data["ram_status"] = ""
+        _free(data["nodes"])
 
         # disk space
         # sample: /dev/vda1 78G 33G 45G 43%
@@ -329,6 +460,12 @@
                         _err if d['subnet_gateway_error'] else ""
 
         _services(data["nodes"])
+        # vcp status
+        # query virsh and prepare for report
+        _vcp_status(data["nodes"])
+
+        # soft net stats
+        _soft_net_stats(data["nodes"])
 
 
 class ReportToFile(object):
diff --git a/scripts/ifs_data.py b/scripts/ifs_data.py
index 119acdb..34263b3 100644
--- a/scripts/ifs_data.py
+++ b/scripts/ifs_data.py
@@ -72,40 +72,46 @@
     if_ipv4 = re.compile(r"^\s{4}inet\ .*$")
     # variable prototypes
     _ifs = {}
-    _if_name = None
+    _name = None
     # get the "ip a" output
     _ifs_raw = shell('ip a')
     for line in _ifs_raw.splitlines():
         _if_data = {}
         if if_start.match(line):
             _tmp = line.split(':')
-            _if_name = _tmp[1].strip()
+            _name = _tmp[1].strip()
+            _name = _name.split('@') if '@' in _name else [_name, ""]
+            _at = _name[1]
+            _name = _name[0]
             _if_options = _tmp[2].strip().split(' ')
-            _lower, _upper, _type = get_linked_devices(_if_name)
-            _if_data['order'] = _tmp[0]
+            _lower, _upper, _type = get_linked_devices(_name)
+            _if_data['if_index'] = _tmp[0]
+            _if_data['at'] = _at
             _if_data['mtu'], _if_options = cut_option("mtu", _if_options)
             _if_data['qlen'], _if_options = cut_option("qlen", _if_options)
             _if_data['state'], _if_options = cut_option("state", _if_options)
             _if_data['other'] = _if_options
             _if_data['ipv4'] = {}
-            _if_data['mac'] = {}
+            _if_data['link'] = {}
             _if_data['type'] = _type
             _if_data['upper'] = _upper
             _if_data['lower'] = _lower
-            _ifs[_if_name] = _if_data
+            _ifs[_name] = _if_data
         elif if_link.match(line):
-            if _if_name is None:
+            if _name is None:
                 continue
             else:
                 _tmp = line.strip().split(' ', 2)
                 _mac_addr = _tmp[1]
                 _options = _tmp[2].split(' ')
                 _brd, _options = cut_option("brd", _options)
-                _ifs[_if_name]['mac'][_mac_addr] = {}
-                _ifs[_if_name]['mac'][_mac_addr]['brd'] = _brd
-                _ifs[_if_name]['mac'][_mac_addr]['other'] = _options
+                _netnsid, _options = cut_option("link-netnsid", _options)
+                _ifs[_name]['link'][_mac_addr] = {}
+                _ifs[_name]['link'][_mac_addr]['brd'] = _brd
+                _ifs[_name]['link'][_mac_addr]['link-netnsid'] = _netnsid
+                _ifs[_name]['link'][_mac_addr]['other'] = _options
         elif if_ipv4.match(line):
-            if _if_name is None:
+            if _name is None:
                 continue
             else:
                 _tmp = line.strip().split(' ', 2)
@@ -113,9 +119,9 @@
                 _options = _tmp[2].split(' ')
                 _brd, _options = cut_option("brd", _options)
                 # TODO: Parse other options, mask, brd, etc...
-                _ifs[_if_name]['ipv4'][_ip] = {}
-                _ifs[_if_name]['ipv4'][_ip]['brd'] = _brd
-                _ifs[_if_name]['ipv4'][_ip]['other'] = _options
+                _ifs[_name]['ipv4'][_ip] = {}
+                _ifs[_name]['ipv4'][_ip]['brd'] = _brd
+                _ifs[_name]['ipv4'][_ip]['other'] = _options
 
     # Collect routes data and try to match it with network
     # Compile regexp for detecting default route
@@ -172,7 +178,7 @@
         print("{0:8} {1:30} {2:18} {3:19} {4:5} {5:4} {6}".format(
             ifs_data[_ifs[_idx]]['type'],
             _ifs[_idx],
-            ",".join(ifs_data[_ifs[_idx]]['mac'].keys()),
+            ",".join(ifs_data[_ifs[_idx]]['link'].keys()),
             ",".join(ifs_data[_ifs[_idx]]['ipv4'].keys()),
             ifs_data[_ifs[_idx]]['mtu'],
             ifs_data[_ifs[_idx]]['state'],
diff --git a/templates/common_styles.j2 b/templates/common_styles.j2
index 66385be..ab1719f 100644
--- a/templates/common_styles.j2
+++ b/templates/common_styles.j2
@@ -88,6 +88,11 @@
         z-index: 1;
     }
 
+    pre {
+        font-family: "Lucida Console", Monaco, monospace;
+        margin-top: 0em;
+    }
+
     .tooltip .repoinfotext {
         right: 0%;
     }
diff --git a/templates/network_check_tmpl.j2 b/templates/network_check_tmpl.j2
index c26a115..a2b17a9 100644
--- a/templates/network_check_tmpl.j2
+++ b/templates/network_check_tmpl.j2
@@ -41,6 +41,13 @@
             padding: 1px;
             margin: 2px;
         }
+        td > .kvm_group {
+            display: grid;
+            grid-template-columns: auto auto auto;
+            padding-left: 0px;
+            padding-right: 0px;
+            margin: 1px;
+        }
         td > .disk_group {
             display: grid;
             grid-template-columns: auto auto auto auto auto;
@@ -48,13 +55,20 @@
             padding-right: 0px;
             margin: 1px;
         }
-        td > .ram_group {
+        td > .ram_group, td > .net_group {
             display: grid;
             grid-template-columns: auto auto auto auto;
             padding-left: 0px;
             padding-right: 0px;
             margin: 1px;
         }
+        td > .net_group {
+            display: grid;
+            grid-template-columns: auto auto auto auto auto;
+            padding-left: 0px;
+            padding-right: 0px;
+            margin: 1px;
+        }
         td > .vcpu_group {
             display: grid;
             grid-template-columns: auto;
@@ -82,10 +96,13 @@
         .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_notes { width: 618px; }
+        .col_node_notes { width: 400px; }
+        .col_cpu_notes { width: 218px; }
+
         .meters {
             display: inline-block;
             margin: 1px;
@@ -99,6 +116,13 @@
             padding: 0px 1px 0px 1px;
             
         }
+        .kvm_id, .kvm_node, .kvm_status {
+            border-width: 0px;
+            background-color: #f0f0f0;
+        }
+        .kvm_id, .kvm_status {
+            text-align: center;
+        }
         .meters > .ok, .disk_group > .ok, .ram_group > .ok{
 			border-color: #80a080;
 			background-color: #efe;
@@ -112,7 +136,8 @@
 			background-color: rgb(250, 135, 135);
         }
         .cpu { border-color: #a0c0a0; background-color: rgb(252, 248, 248); }
-        .ram { border-color: #c0c0a0; background-color: rgb(255, 255, 251); }
+        .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); }
 
         .map_grid {
@@ -149,8 +174,20 @@
 			background-color: white;
         }
         .service_node {
-            background-color: #ddd;
             margin-bottom: 2px;
+            display: flex;
+        }
+        .service_name, .node_name {
+            text-align: center;
+            border-width: 0px;
+			border-style: solid;
+			margin: 1px 1px 1px 1px;
+            padding: 0px 1px 0px 1px;
+            min-width: 250px;
+            border-radius: 10px;
+        }
+        .node_name {
+            background-color: #ddd;
         }
         .service_grid {
             display: grid;
@@ -174,13 +211,13 @@
             border-radius: 10px;
         }
 
-        .service_grid > .on {
+        .service_grid > .on, .service_node > .ok {
             background-color: #8c8;
         }
-        .service_grid > .off {
+        .service_grid > .off, .service_node > .off{
             background-color: #9aa;
         }
-        .service_grid > .fail {
+        .service_grid > .fail, .service_node > .fail {
             background-color: rgb(250, 135, 135);
         }
 
@@ -203,7 +240,7 @@
 <div class="bar">
     <button class="bar-item" onclick="openBar(event, 'nodes')">Nodes</button>
     <button class="bar-item" onclick="openBar(event, 'networks')">Networks</button>
-    <button class="bar-item" onclick="openBar(event, 'services')">Other</button>
+    <button class="bar-item" onclick="openBar(event, 'services')">Services</button>
 </div>
 
 {% macro nodes_page(nodes, id_label) %}
@@ -224,6 +261,14 @@
                     <div class="meter cpu">vCPU</div>
                 </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>
+            </td>
             <td class="head col_ram">
                 <div class="meters">
                     <div class="meter ram">Total</div>
@@ -248,20 +293,28 @@
             <td class="status_{{ _ndat['status'] | node_status_class }}">.</td>
             <td class="head col_name">{{ node }}</td>
             <td class="head col_role">{{ _ndat['role'] }}</td>
-            <td class="head col_vendor">{{ _ndat['virt_vendor'] }}/{{ _ndat['virt_mode'] }}/{{ _ndat['virt_type'] }}</td>
+            <td class="head col_vendor">{{ _ndat['node_type'] }}</td>
             <td class="head col_release">{{ _ndat['linux_arch'] }}/{{ _ndat['linux_codename'] }}</td>
             <td class="head col_kernel">{{ _ndat['kernel'] }}</td>
             <td class="head col_vcpu">
                 <div class="meters vcpu">
-                    <div class="meter cpu">{{ _ndat['cpus'] }}</div>
+                    <div class="meter cpu">{{ _ndat['lscpu']['cpus'] }}</div>
+                </div>
+            </td>
+            <td class="head col_net">
+                <div class="net_group">
+                    <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>
+                    <div class="item net">{{ _ndat['net_stats']['total'][3] }}</div>
                 </div>
             </td>
             <td class="head col_ram">
                 <div class="ram_group">
-                    <div class="item ram">{{ _ndat['ram_total'] }}</div>
-                    <div class="item ram">{{ _ndat['ram_used'] }}</div>
-                    <div class="item ram">{{ _ndat['ram_free'] }}</div>
-                    <div class="item ram {{ _ndat['ram_status'] }}">{{ _ndat['ram_available'] }}</div>
+                    <div class="item ram">{{ _ndat['ram']['total'] }}</div>
+                    <div class="item ram">{{ _ndat['ram']['used'] }}</div>
+                    <div class="item ram">{{ _ndat['ram']['free'] }}</div>
+                    <div class="item ram {{ _ndat['ram']['status'] }}">{{ _ndat['ram']['available'] }}</div>
                 </div>
             </td>
             <td class="head col_disk">
@@ -275,9 +328,33 @@
         </tr>
         <tr class="collapsable" id="info_{{ node }}">
             <td class="status_none"></td>
-            <td class="col_notes" colspan="4">.</td>
+            <td class="col_node_notes" colspan="2">
+                {% if 'virsh' in _ndat %}
+                <div class="kvm_group">
+                {% for kvm_node in _ndat['virsh'].keys() | sort %}
+                    <div class="item kvm_id">{{ _ndat['virsh'][kvm_node]['id'] }}</div>
+                    <div class="item kvm_node">{{ kvm_node }}</div>
+                    <div class="item kvm_status">{{ _ndat['virsh'][kvm_node]['status'] }}</div>
+                {% endfor %}
+                </div>
+                {% endif %}
+            </td>
+            <td class="col_cpu_notes smallgreytext" colspan="2">
+                CPU Model: {{ _ndat['lscpu']['model_name'] }} at {{ _ndat['lscpu']['cpu_mhz'] }}Mhz<br>
+                Virtualization: {{ _ndat['lscpu']['virtualization'] }}
+            </td>
             <td class="col_kernel"></td>
             <td class="col_vcpu"></td>
+            <td class="col_net">
+                <div class="net_group">
+                    {% for cpu in _ndat['net_stats'].keys() | sort %}
+                    <div class="item net">{{ cpu }}</div>
+                    {% for val in _ndat['net_stats'][cpu] %}
+                    <div class="item net">{{ val }}</div>
+                    {% endfor %}
+                    {% endfor %}
+                </div>
+            </td>
             <td class="col_ram"></td>
             <td class="col_disk">
                 <div class="disk_group">
@@ -311,7 +388,7 @@
         {% for d in map[net][node] %}
                 <div class="map_item name">{{ node }}</div>
                 <div class="map_item interface {{ d['interface_error'] }}">{{ d['interface'] }}</div>
-                <div class="map_item note">{{ d['interface_note'] }}</div>
+                <div class="map_item note"><pre>{{ d['interface_map'] | linebreaks }}</pre></div>
                 <div class="map_item ipaddr">{{ d['ip_address'] }}</div>
                 <div class="map_item ipaddr_type">{{ d['address_type'] }}</div>
                 <div class="map_item mtu {{ d['mtu_error'] }}">{{ d['rt_mtu'] }}</div>
@@ -335,14 +412,35 @@
     <hr>
     <div class="services">
             {% for node in nodes.keys() | sort %}
-            <div class="service_node" onclick="toggleClassByID('svc_{{ node }}')" id="svc_{{ node }}_button">{{ node }}</div>
+            <div class="service_node" onclick="toggleClassByID('svc_{{ node }}')" id="svc_{{ node }}_button">
+                <div class="node_name">{{ node }}</div>
+                {% for service in nodes[node]['services'].keys() | sort -%}            
+                {% if service in const['services'] %}
+                {% if not nodes[node]['services'][service] %}
+                    <div class="service_name fail">{{ service }}</div>
+                {% endif %}
+                {% endif%}
+                {% endfor%}
+            </div>
             <div class="collapsable" id="svc_{{ node }}">
                 <div class="service_grid">
             {% for service in nodes[node]['services'].keys() | sort -%}            
+                {% if service in const['services'] %}
                 {% if nodes[node]['services'][service] %}
-                    <div class="service name on">{{ service }}</div>
+                    <div class="service on">{{ service }}</div>
                 {% else %}
-                    <div class="service name off">{{ service }}</div>
+                    <div class="service fail">{{ service }}</div>
+                {% endif %}
+                {% endif%}
+            {% endfor %}
+                    <div class="service"># Other services</div>
+            {% for service in nodes[node]['services'].keys() | sort -%}            
+                {% if service not in const['services'] %}
+                {% if nodes[node]['services'][service] %}
+                    <div class="service on">{{ service }}</div>
+                {% else %}
+                    <div class="service off">{{ service }}</div>
+                {% endif %}
                 {% endif %}
             {% endfor %}
                 </div>