Parsing Ping return codes and saving iteration details

 - ping command detects error types and saves events
 - exit on no packets to send
 - ping now uses runtime map (as reclass can have DHCP set)

Change-Id: Iad66bd90d0c5a43e04fd785f02f8e1c2769dda62
Related-PROD: PROD-28199
diff --git a/cfg_checker/helpers/errors.py b/cfg_checker/helpers/errors.py
index 1f962eb..eaaa1eb 100644
--- a/cfg_checker/helpers/errors.py
+++ b/cfg_checker/helpers/errors.py
@@ -1,4 +1,5 @@
 import os
+from configparser import NoSectionError
 
 from cfg_checker.common import file_utils as fu
 from cfg_checker.common import logger, logger_cli
@@ -62,9 +63,19 @@
                 self._area_code.lower(),
                 filepath=self._conf_filename
             )
-            # it is loaded, update iteration from file
-            self._iteration = self.conf.get_value('iteration', value_type=int)
-            self._iteration += 1
+            # check if there is any values there
+            try:
+                self._iteration = self.conf.get_value(
+                    'iteration',
+                    value_type=int
+                )
+                self._iteration += 1
+            except NoSectionError:
+                self._iteration += 1
+                self.conf.set_value('iteration', self._iteration)
+                self.conf.save_config(filepath=self._conf_filename)
+                logger_cli.debug("... updated config file")
+
         logger_cli.debug(" ... starting iteration {}".format(self._iteration))
 
     def save_iteration_data(self):
@@ -189,14 +200,14 @@
         _total_errors = self.get_errors_total()
 
         _list.append('-'*20)
-        _list.append("{:5d} total errors found\n".format(_total_errors))
+        _list.append("{:5d} total events found\n".format(_total_errors))
         if as_list:
             return _list
         else:
             return "\n".join(_list)
 
     def get_errors(self, as_list=False):
-        _list = ["# Errors"]
+        _list = ["# Events"]
         # Detailed errors
         if self.get_errors_total() > 0:
             # create list of strings with error messages
@@ -204,7 +215,7 @@
                 _list.append(self._format_error(_idx))
                 _list.append("\n")
         else:
-            _list.append("-> No errors")
+            _list.append("-> No events saved")
 
         if as_list:
             return _list
diff --git a/cfg_checker/modules/network/__init__.py b/cfg_checker/modules/network/__init__.py
index b1664a4..6b06022 100644
--- a/cfg_checker/modules/network/__init__.py
+++ b/cfg_checker/modules/network/__init__.py
@@ -125,8 +125,13 @@
     # Should generate network map to console or HTML
     _map = mapper.NetworkMapper()
     reclass = _map.map_network(_map.RECLASS)
+    runtime = _map.map_network(_map.RUNTIME)
+
     _s = [str(_n) for _n in reclass.keys()]
-    logger_cli.info("# Reclass networks list")
+    logger_cli.info("\n# Reclass networks list")
+    logger_cli.info("\n".join(_s))
+    _s = [str(_n) for _n in runtime.keys()]
+    logger_cli.info("\n# Runtime networks list")
     logger_cli.info("\n".join(_s))
 
     return
@@ -141,10 +146,23 @@
     _cidr = args_utils.get_arg(args, "cidr")
     _pinger = pinger.NetworkPinger(mtu=args.mtu, detailed=args.detailed)
 
-    # TODO: Simple ping based on parameters
-    _pinger.ping_nodes(_cidr)
+    _ret = _pinger.ping_nodes(_cidr)
 
-    return
+    if _ret < 0:
+        # no need to save the iterations and summary
+        return
+    else:
+        # save what was collected
+        _pinger.errors.save_iteration_data()
+
+        # print a report
+        _pinger.print_summary()
+
+        # if set, print details
+        if args.detailed:
+            _pinger.print_error_details()
+
+        return
 
 
 def do_trace(args):
diff --git a/cfg_checker/modules/network/mapper.py b/cfg_checker/modules/network/mapper.py
index c44775f..08cc99f 100644
--- a/cfg_checker/modules/network/mapper.py
+++ b/cfg_checker/modules/network/mapper.py
@@ -100,7 +100,7 @@
             # get the reclass value
             _pillar = salt_master.nodes[node]['pillars']['linux']['network']
             # we should be ready if there is no interface in reclass for a node
-            # for example on APT nohde
+            # for example on APT node
             if 'interface' in _pillar:
                 _pillar = _pillar['interface']
             else:
@@ -110,6 +110,8 @@
                     )
                 )
                 continue
+
+            # build map based on IPs
             for _if_name, _if_data in _pillar.iteritems():
                 if 'address' in _if_data:
                     _if = ipaddress.IPv4Interface(
@@ -131,6 +133,8 @@
         # class uses nodes from self.nodes dict
         _confs = {}
 
+        # TODO: parse /etc/network/interfaces
+
         return _confs
 
     def _map_runtime_networks(self):
diff --git a/cfg_checker/modules/network/network_errors.py b/cfg_checker/modules/network/network_errors.py
index 6086be0..80f1199 100644
--- a/cfg_checker/modules/network/network_errors.py
+++ b/cfg_checker/modules/network/network_errors.py
@@ -16,6 +16,10 @@
     NET_NODE_NON_RESPONSIVE = next(_c)
     NET_NODE_UNEXPECTED_IF = next(_c)
     NET_NO_RUNTIME_NETWORK = next(_c)
+    NET_PING_SUCCESS = next(_c)
+    NET_PING_TIMEOUT = next(_c)
+    NET_PING_ERROR = next(_c)
+    NET_PING_NOT_RESOLVED = next(_c)
 
     def __init__(self):
         super(NetworkErrors, self).__init__("NET")
@@ -52,6 +56,22 @@
             self.NET_NO_RUNTIME_NETWORK,
             "Reclass network not found in Runtime"
         )
+        self.add_error_type(
+            self.NET_PING_SUCCESS,
+            "Network Ping successfull"
+        )
+        self.add_error_type(
+            self.NET_PING_TIMEOUT,
+            "Ping Timeout from source to target"
+        )
+        self.add_error_type(
+            self.NET_PING_ERROR,
+            "Error while conducting ping"
+        )
+        self.add_error_type(
+            self.NET_PING_NOT_RESOLVED,
+            "Host not resolved while conducting Ping"
+        )
 
 
 del _c
diff --git a/cfg_checker/modules/network/pinger.py b/cfg_checker/modules/network/pinger.py
index 6d0287b..351c05a 100644
--- a/cfg_checker/modules/network/pinger.py
+++ b/cfg_checker/modules/network/pinger.py
@@ -1,18 +1,16 @@
 import ipaddress
 import json
 
-from cfg_checker.common import logger, logger_cli
+from cfg_checker.common import logger_cli
 from cfg_checker.helpers.console_utils import Progress
 from cfg_checker.modules.network.mapper import NetworkMapper
+from cfg_checker.modules.network.network_errors import NetworkErrors
 from cfg_checker.nodes import salt_master
 
 
-# ping -s 9072 -M do -n -c 1 -w 1 -W 1 ctl01
-
-
 # This is independent class with a salt.nodes input
 class NetworkPinger(object):
-    def __init__(self, mtu=None, detailed=False):
+    def __init__(self, mtu=None, detailed=False, errors_class=None):
         logger_cli.info("# Initializing")
         # all active nodes in the cloud
         self.target_nodes = salt_master.get_nodes()
@@ -22,11 +20,17 @@
         self.packet_size = int(self.target_mtu) - 20 - 8
         self.detailed_summary = detailed
 
+        if errors_class:
+            self.errors = errors_class
+        else:
+            logger_cli.debug("... init error logs folder")
+            self.errors = NetworkErrors()
+
     def _collect_node_addresses(self, target_net):
         # use reclass model and standard methods
         # to create list of nodes with target network
-        _mapper = NetworkMapper()
-        _reclass = _mapper.map_network(_mapper.RECLASS)
+        _mapper = NetworkMapper(errors_class=self.errors)
+        _reclass = _mapper.map_network(_mapper.RUNTIME)
         if target_net in _reclass:
             return _reclass[target_net]
         else:
@@ -90,11 +94,17 @@
                             pass
         logger_cli.info("-> {} packets to send".format(_count))
 
+        if not _count:
+            logger_cli.warning(
+                "\n# WARNING: No packets to send for '{}', "
+                "check network configuration\n".format(network_cidr_str)
+            )
+
+            return -1
+
         # do ping of packets
         logger_cli.info("# Pinging nodes: MTU={}".format(self.target_mtu))
         salt_master.prepare_script_on_active_nodes("ping.py")
-        _errors = []
-        _success = []
         _progress = Progress(_count)
         _progress_index = 0
         _node_index = 0
@@ -144,37 +154,53 @@
                     if len(_params["stderr"]) > 0:
                         _stderr = "stderr:\n{}\n".format(_params["stderr"])
 
-                    if _params["returncode"]:
-                        _errors.append("FAIL: {}{}{}".format(
-                            _body,
-                            _stdout,
-                            _stderr
-                        ))
+                    if not _params["returncode"]:
+                        # 0
+                        self.errors.add_error(
+                            self.errors.NET_PING_SUCCESS,
+                            ping_path=_body,
+                            stdout=_stdout,
+                            stderr=_stderr
+                        )
+                    elif _params["returncode"] == 68:
+                        # 68 is a 'can't resove host error'
+                        self.errors.add_error(
+                            self.errors.NET_PING_NOT_RESOLVED,
+                            ping_path=_body,
+                            stdout=_stdout,
+                            stderr=_stderr
+                        )
+                    elif _params["returncode"] > 1:
+                        # >1 is when no actial (any) response
+                        self.errors.add_error(
+                            self.errors.NET_PING_ERROR,
+                            ping_path=_body,
+                            stdout=_stdout,
+                            stderr=_stderr
+                        )
                     else:
-                        _success.append("PASS: {}{}{}".format(
-                            _body,
-                            _stdout,
-                            _stderr
-                        ))
+                        # 1 is for timeouts amd/or packet lost
+                        self.errors.add_error(
+                            self.errors.NET_PING_TIMEOUT,
+                            ping_path=_body,
+                            stdout=_stdout,
+                            stderr=_stderr
+                        )
 
             # Parse results back in place
             src_data["targets"] = _result
 
         _progress.end()
 
-        if self.detailed_summary:
-            logger_cli.info("\n{:=^8s}".format("PASS"))
-            logger_cli.info("\n".join(_success))
-        else:
-            logger.info("\n{:=^8s}".format("PASS"))
-            logger.info("\n".join(_success))
-        if len(_errors) > 0:
-            logger_cli.info("\n{:=^8s}".format("FAIL"))
-            logger_cli.info("\n".join(_errors))
+        return 0
 
+    def print_summary(self):
+        logger_cli.info(self.errors.get_summary(print_zeros=False))
+
+    def print_details(self):
+        # Detailed errors
         logger_cli.info(
-            "# {} failed, {} passed".format(
-                len(_errors),
-                len(_success)
+            "\n{}\n".format(
+                self.errors.get_errors()
             )
         )
diff --git a/cfg_checker/modules/packages/repos.py b/cfg_checker/modules/packages/repos.py
index 334cbb5..025703e 100644
--- a/cfg_checker/modules/packages/repos.py
+++ b/cfg_checker/modules/packages/repos.py
@@ -821,29 +821,19 @@
             # <10symbols> \t <md5> \t sorted headers with no tag
             # ...
             # section
-            _ss = _p.keys()
-            _ss.sort()
-            for _s in _ss:
-                _apps = _p[_s].keys()
-                _apps.sort()
+            for _s in sorted(_p):
                 # app
-                for _a in _apps:
+                for _a in sorted(_p[_s]):
                     _o = ""
                     _mm = []
                     # get and sort tags
-                    _vs = _p[_s][_a].keys()
-                    _vs.sort()
-                    for _v in _vs:
+                    for _v in sorted(_p[_s][_a]):
                         _o += "\n" + " "*8 + _v + ':\n'
                         # get and sort tags
-                        _mds = _p[_s][_a][_v].keys()
-                        _mds.sort()
-                        for _md5 in _mds:
+                        for _md5 in sorted(_p[_s][_a][_v]):
                             _o += " "*16 + _md5 + "\n"
                             # get and sort repo headers
-                            _rr = _p[_s][_a][_v][_md5].keys()
-                            _rr.sort()
-                            for _r in _rr:
+                            for _r in sorted(_p[_s][_a][_v][_md5]):
                                 _o += " "*24 + _r.replace('_', ' ') + '\n'
                                 _m = _p[_s][_a][_v][_md5][_r]["maintainer"]
                                 if _m not in _mm: