Network check fixes
- Proper network mapping
- Proper reclass lookup
- VIP detection
- Simple error gathering
- IP shown as 'exploded', i.e. in CIDR format
- MTU matching and detection
- Errors class for handling errors, including codes and indices
- Summary and detailed errors view
- Flake8 refactoring
Change-Id: I8ee37d345bdc21c7ad930bf8305acd28f8c121c8
Related-PROD: PROD-28199
diff --git a/cfg_checker/modules/network/checker.py b/cfg_checker/modules/network/checker.py
index b0056c8..25060a6 100644
--- a/cfg_checker/modules/network/checker.py
+++ b/cfg_checker/modules/network/checker.py
@@ -1,28 +1,63 @@
-import json
-import os
-import sys
import ipaddress
+import json
-from copy import deepcopy
+from cfg_checker.common import logger_cli
+from cfg_checker.modules.network.network_errors import NetworkErrors
+from cfg_checker.nodes import SaltNodes
from cfg_checker.reports import reporter
-from cfg_checker.common import utils, const
-from cfg_checker.common import config, logger, logger_cli, pkg_dir
-from cfg_checker.common import salt_utils
-from cfg_checker.nodes import SaltNodes, node_tmpl
class NetworkChecker(SaltNodes):
- @staticmethod
- def _map_network_for_host(host, if_class, net_list, data):
- if if_class.network in net_list.keys():
- # There is a network
- net_list[if_class.network][host] = data
+ def __init__(self):
+ super(NetworkChecker, self).__init__()
+ self.errors = NetworkErrors()
+
+ # adding net data to tree
+ def _add_data(self, _list, _n, _h, _d):
+ if _n not in _list:
+ _list[_n] = {}
+ _list[_n][_h] = [_d]
+ elif _h not in _list[_n]:
+ # there is no such host, just create it
+ _list[_n][_h] = [_d]
else:
- # create subnet key
- net_list[if_class.network] = {}
- # add the host to the dict
- net_list[if_class.network][host] = data
+ # there is such host... this is an error
+ self.errors.add_error(
+ self.errors.NET_DUPLICATE_IF,
+ host=_h,
+ dup_if=_d['name']
+ )
+ _list[_n][_h].append(_d)
+
+ # TODO: refactor map creation. Build one map instead of two separate
+ def _map_network_for_host(self, host, if_class, net_list, data):
+ # filter networks for this IF IP
+ _nets = [n for n in net_list.keys() if if_class.ip in n]
+ _masks = [n.netmask for n in _nets]
+ if len(_nets) > 1:
+ # There a multiple network found for this IP, Error
+ self.errors.add_error(
+ self.errors.NET_SUBNET_INTERSECT,
+ host=host,
+ networks="; ".join(_nets)
+ )
+ # check mask match
+ if len(_nets) > 0 and if_class.netmask not in _masks:
+ self.errors.add_error(
+ self.errors.NET_MASK_MISMATCH,
+ host=host,
+ if_name=data['name'],
+ if_cidr=if_class.exploded,
+ if_mapped_networks=", ".join([str(_n) for _n in _nets])
+ )
+
+ if len(_nets) < 1:
+ self._add_data(net_list, if_class.network, host, data)
+ else:
+ # add all data
+ for net in _nets:
+ self._add_data(net_list, net, host, data)
return net_list
@@ -33,8 +68,11 @@
:return: none
"""
logger_cli.info("# Mapping node runtime network data")
- _result = self.execute_script_on_active_nodes("ifs_data.py", args=["json"])
-
+ _result = self.execute_script_on_active_nodes(
+ "ifs_data.py",
+ args=["json"]
+ )
+ self.stage = "Runtime"
for key in self.nodes.keys():
# check if we are to work with this node
if not self.is_node_available(key):
@@ -54,6 +92,7 @@
))
logger_cli.info("-> done collecting networks data")
+ # TODO: Mimic reclass structure for easy compare
logger_cli.info("### Building network tree")
# match interfaces by IP subnets
_all_nets = {}
@@ -66,26 +105,35 @@
if net_name in ['lo']:
# skip the localhost
continue
- _ip4s = net_data['ipv4']
- for _ip_str in _ip4s.keys():
- # create interface class
+ # get data and make sure that wide mask goes first
+ _ip4s = sorted(
+ net_data['ipv4'],
+ key=lambda s: s[s.index('/'):]
+ )
+ for _ip_str in _ip4s:
+ # create interface class
_if = ipaddress.IPv4Interface(_ip_str)
- net_data['name'] = net_name
- net_data['if'] = _if
-
- _all_nets = self._map_network_for_host(
- host,
- _if,
- _all_nets,
- net_data
- )
+ if 'name' not in net_data:
+ net_data['name'] = net_name
+ if 'ifs' not in net_data:
+ net_data['ifs'] = [_if]
+ # map it
+ _all_nets = self._map_network_for_host(
+ host,
+ _if,
+ _all_nets,
+ net_data
+ )
+ else:
+ # data is already there, just add VIP
+ net_data['ifs'].append(_if)
# save collected info
self.all_nets = _all_nets
-
def collect_reclass_networks(self):
logger_cli.info("# Mapping reclass networks")
+ self.stage = "Reclass"
# Get networks from reclass and mark them
_reclass_nets = {}
# Get required pillars
@@ -101,9 +149,11 @@
if 'interface' in _pillar:
_pillar = _pillar['interface']
else:
- logger_cli.info("...skipping node '{}', no IF section in reclass".format(
- node
- ))
+ logger_cli.info(
+ "... node '{}' skipped, no IF section in reclass".format(
+ node
+ )
+ )
continue
for _if_name, _if_data in _pillar.iteritems():
if 'address' in _if_data:
@@ -111,7 +161,7 @@
_if_data['address'] + '/' + _if_data['netmask']
)
_if_data['name'] = _if_name
- _if_data['if'] = _if
+ _if_data['ifs'] = [_if]
_reclass_nets = self._map_network_for_host(
node,
@@ -122,7 +172,6 @@
self.reclass_nets = _reclass_nets
-
def print_network_report(self):
"""
Create text report for CLI
@@ -132,7 +181,8 @@
_all_nets = self.all_nets.keys()
logger_cli.info("# Reclass networks")
logger_cli.info(
- " {0:17} {1:25}: {2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
+ " {0:17} {1:25}: "
+ "{2:19} {3:5}{4:10} {5}{6} {7} / {8} / {9}".format(
"Hostname",
"IF",
"IP",
@@ -145,7 +195,8 @@
"rcGate"
)
)
-
+ # TODO: Move matching to separate function
+ self.stage = "Matching"
_reclass = [n for n in _all_nets if n in self.reclass_nets]
for network in _reclass:
# shortcuts
@@ -154,7 +205,7 @@
names = sorted(self.all_nets[network].keys())
for hostname in names:
if not self.is_node_available(hostname, log=False):
- logger_cli.info(
+ logger_cli.info(
" {0:17} {1}".format(
hostname.split('.')[0],
"... no data for the node"
@@ -167,8 +218,8 @@
if not _route:
_gate = "no route!"
else:
- _gate = _route['gateway'] if _route['gateway'] else "empty"
-
+ _gate = _route['gateway'] if _route['gateway'] else "-"
+
# get the default gateway
if 'default' in _routes:
_d_gate = ipaddress.IPv4Address(
@@ -179,45 +230,107 @@
_d_gate_str = _d_gate if _d_gate else "No default gateway!"
_a = self.all_nets[network][hostname]
- # Check if reclass has such network
- if hostname in self.reclass_nets[network]:
- _r = self.reclass_nets[network][hostname]
- else:
- # Supply empty dict if there is no reclass gathered
- _r = {}
-
- # Take gateway parameter for this IF
- # from corresponding reclass record
- # TODO: Update gateway search mechanism
- if not self.is_node_available(hostname):
- _r_gate = "-"
- elif _a['if'].network not in self.reclass_nets:
- _r_gate = "no IF in reclass!"
- elif not hostname in self.reclass_nets[_a['if'].network]:
- _r_gate = "no IF on node in reclass!"
- else:
- _rd = self.reclass_nets[_a['if'].network][hostname]
- _r_gate = _rd['gateway'] if 'gateway' in _rd else "empty"
+ for _host in _a:
+ for _if in _host['ifs']:
+ # get proper reclass
+ _ip_str = str(_if.exploded)
+ _r = {}
+ for _item in self.reclass_nets[network][hostname]:
+ for _item_ifs in _item['ifs']:
+ if _ip_str == str(_item_ifs.exploded):
+ _r = _item
- if not 'enabled' in _r:
- _enabled = "no record!"
- else:
- _enabled = "(enabled)" if _r['enabled'] else "(disabled)"
- _text = "{0:25}: {1:19} {2:5}{3:10} {4:4}{5:10} {6} / {7} / {8}".format(
- _a['name'],
- str(_a['if'].ip),
- _a['mtu'],
- '('+str(_r['mtu'])+')' if 'mtu' in _r else '(unset!)',
- _a['state'],
- _enabled,
- _gate,
- _d_gate_str,
- _r_gate
- )
- logger_cli.info(
- " {0:17} {1}".format(hostname.split('.')[0], _text)
- )
-
+ # check if node is UP
+ if not self.is_node_available(hostname):
+ _r_gate = "-"
+ # get proper network from reclass
+ else:
+ # Lookup match for the ip
+ _r_gate = "no IF in reclass!"
+ # get all networks with this hostname
+ _rn = self.reclass_nets
+ _nets = filter(
+ lambda n: hostname in _rn[n].keys(),
+ self.reclass_nets
+ )
+ _rd = None
+ for _item in _nets:
+ # match ip
+ _r_dat = self.reclass_nets[_item][hostname]
+ for _r_ifs in _r_dat:
+ for _r_if in _r_ifs['ifs']:
+ if _if.ip == _r_if.ip:
+ _rd = _r_ifs
+ break
+ if _rd:
+ _gs = 'gateway'
+ _e = "empty"
+ _r_gate = _rd[_gs] if _gs in _rd else _e
+ break
+
+ # IF status in reclass
+ if 'enabled' not in _r:
+ _enabled = "no record!"
+ else:
+ _e = "enabled"
+ _d = "disabled"
+ _enabled = "("+_e+")" if _r[_e] else "("+_d+")"
+
+ _name = _host['name']
+ _rc_mtu = _r['mtu'] if 'mtu' in _r else None
+
+ # Check if this is a VIP
+ if _if.network.prefixlen == 32:
+ _name = " "*20
+ _ip_str += " VIP"
+ _rc_mtu = "(-)"
+ _enabled = "(-)"
+ _r_gate = "-"
+
+ # Check if this is a default MTU
+ elif _host['mtu'] == '1500':
+ # reclass is empty if MTU is untended to be 1500
+ _rc_mtu = "(-)"
+ elif _rc_mtu:
+ # if there is an MTU value, match it
+ if _host['mtu'] != str(_rc_mtu):
+ self.errors.add_error(
+ self.errors.NET_MTU_MISMATCH,
+ host=hostname,
+ if_name=_name,
+ if_cidr=_ip_str,
+ reclass_mtu=_rc_mtu,
+ runtime_mtu=_host['mtu']
+ )
+ else:
+ # there is no MTU value in reclass
+ self.errors.add_error(
+ self.errors.NET_MTU_EMPTY,
+ host=hostname,
+ if_name=_name,
+ if_cidr=_ip_str,
+ if_mtu=_host['mtu']
+ )
+
+ _text = "{0:25}: {1:19} {2:5}{3:10} {4:4}{5:10} {6} "
+ "/ {7} / {8}".format(
+ _name,
+ _ip_str,
+ _host['mtu'],
+ str(_rc_mtu) if _rc_mtu else "(No!)",
+ _host['state'],
+ _enabled,
+ _gate,
+ _d_gate_str,
+ _r_gate
+ )
+ logger_cli.info(
+ " {0:17} {1}".format(
+ hostname.split('.')[0],
+ _text
+ )
+ )
+
logger_cli.info("\n# Other networks")
_other = [n for n in _all_nets if n not in self.reclass_nets]
for network in _other:
@@ -225,17 +338,41 @@
names = sorted(self.all_nets[network].keys())
for hostname in names:
- _text = "{0:25}: {1:19} {2:5} {3:4}".format(
- self.all_nets[network][hostname]['name'],
- str(self.all_nets[network][hostname]['if'].ip),
- self.all_nets[network][hostname]['mtu'],
- self.all_nets[network][hostname]['state']
- )
- logger_cli.info(
- " {0:17} {1}".format(hostname.split('.')[0], _text)
- )
+ for _n in self.all_nets[network][hostname]:
+ _ifs = [str(ifs.ip) for ifs in _n['ifs']]
+ _text = "{0:25}: {1:19} {2:5} {3:4}".format(
+ _n['name'],
+ ", ".join(_ifs),
+ _n['mtu'],
+ _n['state']
+ )
+ logger_cli.info(
+ " {0:17} {1}".format(hostname.split('.')[0], _text)
+ )
-
+ def print_summary(self):
+ _total_errors = self.errors.get_errors_total()
+ # Summary
+ logger_cli.info(
+ "\n{:=^8s}\n{:^8s}\n{:=^8s}".format(
+ "=",
+ "Totals",
+ "="
+ )
+ )
+ logger_cli.info(self.errors.get_summary(print_zeros=False))
+ logger_cli.info('-'*20)
+ logger_cli.info("{:5d} total errors found\n".format(_total_errors))
+
+ def print_error_details(self):
+ # Detailed errors
+ if self.errors.get_errors_total() > 0:
+ logger_cli.info("\n# Errors")
+ for _msg in self.errors.get_errors_as_list():
+ logger_cli.info("{}\n".format(_msg))
+ else:
+ logger_cli.info("-> No errors\n")
+
def create_html_report(self, filename):
"""
Create static html showing network schema-like report