Unified command execution and unit tests

- All arguments inits moved to own clases
- Added unified way to execute commands
- Unit test structure and very basic tests
- Command line script to test coverage
- Argument parsers moved to corresponding commands
- Automatic parsers and command mapping

Change-Id: Id099d14702d9590729583dfd9574bd57022efac5
Related-PROD: PROD-28199
diff --git a/cfg_checker/cfg_check.py b/cfg_checker/cfg_check.py
index 22125f6..37f9957 100644
--- a/cfg_checker/cfg_check.py
+++ b/cfg_checker/cfg_check.py
@@ -2,10 +2,7 @@
 import sys
 from logging import DEBUG, INFO
 
-from cfg_checker.cli.command import execute_command
-from cfg_checker.cli.network import init_network_parser
-from cfg_checker.cli.package import init_package_parser
-from cfg_checker.cli.reclass import init_reclass_parser
+from cfg_checker.cli.command import execute_command, helps, parsers
 from cfg_checker.common import config, logger, logger_cli
 from cfg_checker.helpers.args_utils import MyParser
 
@@ -37,26 +34,13 @@
     )
     subparsers = parser.add_subparsers(dest='command')
 
-    # package
-    pkg_parser = subparsers.add_parser(
-        'packages',
-        help="Package versions check (Candidate vs Installed)"
-    )
-    init_package_parser(pkg_parser)
-
-    # networking
-    net_parser = subparsers.add_parser(
-        'network',
-        help="Network infrastructure checks and reports"
-    )
-    init_network_parser(net_parser)
-
-    # reclass
-    reclass_parser = subparsers.add_parser(
-        'reclass',
-        help="Reclass related checks and reports"
-    )
-    init_reclass_parser(reclass_parser)
+    # create parsers
+    for _command in helps.keys():
+        _parser = subparsers.add_parser(
+            _command,
+            help=helps[_command]
+        )
+        parsers[_command](_parser)
 
     # parse arguments
     try:
diff --git a/cfg_checker/cli/command.py b/cfg_checker/cli/command.py
index 0a9c2a2..e6d9cd9 100644
--- a/cfg_checker/cli/command.py
+++ b/cfg_checker/cli/command.py
@@ -1,19 +1,33 @@
+import pkgutil
 import sys
 import traceback
 
-from cfg_checker.common import logger_cli
+from cfg_checker.common import config, logger, logger_cli
 from cfg_checker.common.exception import CheckerException
+from cfg_checker.helpers.args_utils import MyParser
 
-# TODO: auto-create types for each command
-package_command_types = ['report']
-network_command_types = ['check', 'report']
-reclass_command_types = ['list', 'diff']
+main_pkg_name = __name__.split('.')[0]
+mods_package_name = "modules"
+mods_import_path = main_pkg_name + '.' + mods_package_name
+mods_prefix = mods_import_path + '.'
 
-commands = {
-    'packages': package_command_types,
-    'network': network_command_types,
-    'reclass': reclass_command_types
-}
+commands = {}
+parsers = {}
+helps = {}
+# Pure dynamic magic, loading all 'do_*' methods from available modules
+_m = __import__(mods_import_path, fromlist=[main_pkg_name])
+for _imp, modName, isMod in pkgutil.iter_modules(_m.__path__, mods_prefix):
+    # iterate all packages, add to dict
+    if isMod:
+        # load module
+        _p = _imp.find_module(modName).load_module(modName)
+        # create a shortname
+        mod_name = modName.split('.')[-1]
+        # A package! Create it and add commands
+        commands[mod_name] = \
+            [_n[3:] for _n in dir(_p) if _n.startswith("do_")]
+        parsers[mod_name] = getattr(_p, 'init_parser')
+        helps[mod_name] = getattr(_p, 'command_help')
 
 
 def execute_command(args, command):
@@ -34,7 +48,7 @@
         # form function name to call
         _method_name = "do_" + args.type
         _target_module = __import__(
-            "cfg_checker.modules."+command,
+            mods_prefix + command,
             fromlist=[""]
         )
         _method = getattr(_target_module, _method_name)
@@ -57,3 +71,23 @@
             ))
         ))
         return 1
+
+
+def cli_command(_title, _name):
+    my_parser = MyParser(_title)
+    parsers[_name](my_parser)
+
+    # parse arguments
+    try:
+        args = my_parser.parse_args()
+    except TypeError:
+        logger_cli.info("\n# Please, check arguments")
+        sys.exit(0)
+
+    # force use of sudo
+    config.ssh_uses_sudo = True
+
+    # Execute the command
+    result = execute_command(args, _name)
+    logger.debug(result)
+    sys.exit(result)
diff --git a/cfg_checker/cli/network.py b/cfg_checker/cli/network.py
index b53311d..5c5a4e2 100644
--- a/cfg_checker/cli/network.py
+++ b/cfg_checker/cli/network.py
@@ -1,59 +1,12 @@
-import sys
-
-from cfg_checker.common import config, logger, logger_cli
-from cfg_checker.helpers.args_utils import MyParser
-
-from command import execute_command
+from command import cli_command
 
 
-def init_network_parser(_parser):
-    # network subparser
-    net_subparsers = _parser.add_subparsers(dest='type')
-
-    net_check_parser = net_subparsers.add_parser(
-        'check',
-        help="Do network check and print the result"
+def entrypoint():
+    cli_command(
+        "# Mirantis Cloud Network checker",
+        'network'
     )
 
-    net_check_parser.add_argument(
-        '--detailed',
-        action="store_true", default=False,
-        help="Print error details after summary"
-    )
-
-    net_report_parser = net_subparsers.add_parser(
-        'report',
-        help="Generate network check report"
-    )
-
-    net_report_parser.add_argument(
-        '--html',
-        metavar='network_html_filename',
-        help="HTML filename to save report"
-    )
-
-    return _parser
-
-
-def cli_network():
-    net_parser = MyParser("# Mirantis Cloud Network checker")
-    init_network_parser(net_parser)
-
-    # parse arguments
-    try:
-        args = net_parser.parse_args()
-    except TypeError:
-        logger_cli.info("\n# Please, check arguments")
-        sys.exit(0)
-
-    # force use of sudo
-    config.ssh_uses_sudo = True
-
-    # Execute the command
-    result = execute_command(args, 'network')
-    logger.debug(result)
-    sys.exit(result)
-
 
 if __name__ == '__main__':
-    cli_network()
+    entrypoint()
diff --git a/cfg_checker/cli/package.py b/cfg_checker/cli/package.py
deleted file mode 100644
index 5fe7e0b..0000000
--- a/cfg_checker/cli/package.py
+++ /dev/null
@@ -1,57 +0,0 @@
-import sys
-
-from cfg_checker.common import config, logger, logger_cli
-from cfg_checker.helpers.args_utils import MyParser
-
-from command import execute_command
-
-
-def init_package_parser(_parser):
-    # packages subparser
-    pkg_subparsers = _parser.add_subparsers(dest='type')
-
-    pkg_report_parser = pkg_subparsers.add_parser(
-        'report',
-        help="Report package versions to HTML file"
-    )
-    pkg_report_parser.add_argument(
-        '--full',
-        action="store_true", default=False,
-        help="HTML report will have all of the packages, not just errors"
-    )
-    pkg_report_parser.add_argument(
-        '--html',
-        metavar='packages_html_filename',
-        help="HTML filename to save report"
-    )
-    pkg_report_parser.add_argument(
-        '--csv',
-        metavar='packages_csv_filename',
-        help="CSV filename to save report"
-    )
-
-    return _parser
-
-
-def cli_package():
-    pkg_parser = MyParser("# Mirantis Cloud Package checker")
-    init_package_parser(pkg_parser)
-
-    # parse arguments
-    try:
-        args = pkg_parser.parse_args()
-    except TypeError:
-        logger_cli.info("\n# Please, check arguments")
-        sys.exit(0)
-
-    # force use of sudo
-    config.ssh_uses_sudo = True
-
-    # Execute the command
-    result = execute_command(args, 'packages')
-    logger.debug(result)
-    sys.exit(result)
-
-
-if __name__ == '__main__':
-    cli_package()
diff --git a/cfg_checker/cli/packages.py b/cfg_checker/cli/packages.py
new file mode 100644
index 0000000..c44e5bc
--- /dev/null
+++ b/cfg_checker/cli/packages.py
@@ -0,0 +1,12 @@
+from command import cli_command
+
+
+def entrypoint():
+    cli_command(
+        "# Mirantis Cloud Package checker",
+        'packages'
+    )
+
+
+if __name__ == '__main__':
+    entrypoint()
diff --git a/cfg_checker/cli/reclass.py b/cfg_checker/cli/reclass.py
index fc5961a..24eb8e2 100644
--- a/cfg_checker/cli/reclass.py
+++ b/cfg_checker/cli/reclass.py
@@ -1,67 +1,12 @@
-import sys
-
-from cfg_checker.common import config, logger, logger_cli
-from cfg_checker.helpers.args_utils import MyParser
-
-from command import execute_command
+from command import cli_command
 
 
-def init_reclass_parser(_parser):
-    # reclass subparsers
-    reclass_subparsers = _parser.add_subparsers(dest='type')
-    reclass_list_parser = reclass_subparsers.add_parser(
-        'list',
-        help="List models available to compare"
+def entrypoint():
+    cli_command(
+        '# Mirantis Cloud Reclass comparer"',
+        'packages'
     )
-    reclass_list_parser.add_argument(
-        "-p",
-        "--models-path",
-        default="/srv/salt/",
-        help="Global path to search models in"
-    )
-
-    reclass_diff_parser = reclass_subparsers.add_parser(
-        'diff',
-        help="List models available to compare"
-    )
-    reclass_diff_parser.add_argument(
-        "--model1",
-        required=True,
-        help="Model A <path>. Model name is the folder name"
-    )
-    reclass_diff_parser.add_argument(
-        "--model2",
-        required=True,
-        help="Model B <path>. Model name is the folder name"
-    )
-    reclass_diff_parser.add_argument(
-        '--html',
-        metavar='reclass_html_filename',
-        help="HTML filename to save report"
-    )
-
-    return _parser
-
-
-def cli_reclass():
-    net_parser = MyParser("# Mirantis Cloud Reclass comparer")
-    init_reclass_parser(net_parser)
-
-    # parse arguments
-    try:
-        args = net_parser.parse_args()
-    except TypeError:
-        logger_cli.info("\n# Please, check arguments")
-        sys.exit(0)
-
-    # force use of sudo
-    config.ssh_uses_sudo = True
-
-    # Execute the command
-    result = execute_command(args, 'reclass')
-    logger.debug(result)
-    sys.exit(result)
 
 
 if __name__ == "__main__":
-    cli_reclass()
+    entrypoint()
diff --git a/cfg_checker/helpers/args_utils.py b/cfg_checker/helpers/args_utils.py
index daf55b1..1661c96 100644
--- a/cfg_checker/helpers/args_utils.py
+++ b/cfg_checker/helpers/args_utils.py
@@ -34,7 +34,19 @@
         raise ConfigException("'{}' not exists".format(path))
 
 
-def get_report_type_and_filename(args):
+def get_network_map_type_and_filename(args):
+    if args.html or args.text:
+        if args.html and args.text:
+            raise ConfigException("Multuple report types not supported")
+        if args.html is not None:
+            return 'html', args.html
+        if args.text is not None:
+            return 'text', args.text
+    else:
+        return 'console', None
+
+
+def get_package_report_type_and_filename(args):
     if args.html or args.csv:
         if args.html and args.csv:
             raise ConfigException("Multuple report types not supported")
diff --git a/cfg_checker/helpers/errors.py b/cfg_checker/helpers/errors.py
index ca8a8da..1f962eb 100644
--- a/cfg_checker/helpers/errors.py
+++ b/cfg_checker/helpers/errors.py
@@ -200,7 +200,7 @@
         # Detailed errors
         if self.get_errors_total() > 0:
             # create list of strings with error messages
-            for _idx in range(1, self._index - 1):
+            for _idx in range(1, self._index):
                 _list.append(self._format_error(_idx))
                 _list.append("\n")
         else:
diff --git a/cfg_checker/modules/network/__init__.py b/cfg_checker/modules/network/__init__.py
index 78df6c6..075dccf 100644
--- a/cfg_checker/modules/network/__init__.py
+++ b/cfg_checker/modules/network/__init__.py
@@ -3,6 +3,37 @@
 
 import checker
 
+command_help = "Network infrastructure checks and reports"
+
+
+def init_parser(_parser):
+    # network subparser
+    net_subparsers = _parser.add_subparsers(dest='type')
+
+    net_check_parser = net_subparsers.add_parser(
+        'check',
+        help="Do network check and print the result"
+    )
+
+    net_check_parser.add_argument(
+        '--detailed',
+        action="store_true", default=False,
+        help="Print error details after summary"
+    )
+
+    net_report_parser = net_subparsers.add_parser(
+        'report',
+        help="Generate network check report"
+    )
+
+    net_report_parser.add_argument(
+        '--html',
+        metavar='network_html_filename',
+        help="HTML filename to save report"
+    )
+
+    return _parser
+
 
 def _prepare_check():
     _checker_class = checker.NetworkChecker()
@@ -11,7 +42,16 @@
     return _checker_class
 
 
+def _prepare_map():
+    _map_class = None
+
+    return _map_class
+
+
 def do_check(args):
+    # Net Checks
+    # should not print map, etc...
+    # Just bare summary and errors
     logger_cli.info("# Network check to console")
     netChecker = _prepare_check()
     netChecker.print_network_report()
@@ -28,7 +68,11 @@
 
 
 def do_report(args):
-    logger_cli.info("# Network report")
+    # Network Report
+    # should generate Static HTML page
+    # with node/network map and values
+
+    logger_cli.info("# Network report (check, node map")
 
     _filename = args_utils.get_arg(args, 'html')
 
@@ -36,3 +80,35 @@
     netChecker.create_html_report(_filename)
 
     return
+
+
+def do_map(args):
+    # Network Map
+    # Should generate network map to console or HTML
+    logger_cli.info("# Network report")
+
+    _type, _filename = args_utils.get_network_map_type_and_filename(args)
+    # networkMap = _prepare_map
+
+    # TODO: Create map class to generate network map
+
+    return
+
+
+def do_ping(args):
+    # Network pinger
+    # Checks if selected nodes are pingable
+    # with a desireble parameters: MTU, Frame, etc
+
+    # TODO: Simple ping based on parameters
+
+    return
+
+
+def do_trace(args):
+    # Network packet tracer
+    # Check if packet is delivered to proper network host
+
+    # TODO: Packet tracer
+
+    return
diff --git a/cfg_checker/modules/network/checker.py b/cfg_checker/modules/network/checker.py
index 48e7acb..7e6a239 100644
--- a/cfg_checker/modules/network/checker.py
+++ b/cfg_checker/modules/network/checker.py
@@ -115,6 +115,11 @@
                 for _ip_str in _ip4s:
                     # create interface class
                     _if = ipaddress.IPv4Interface(_ip_str)
+                    # check if this is a VIP
+                    # ...all those will have /32 mask
+                    net_data['vip'] = None
+                    if _if.network.prefixlen == 32:
+                        net_data['vip'] = str(_if.exploded)
                     if 'name' not in net_data:
                         net_data['name'] = net_name
                     if 'ifs' not in net_data:
@@ -303,47 +308,44 @@
 
                         _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:
-                            _rc_mtu_s = str(_rc_mtu)
-                            # if there is an MTU value, match it
-                            if _host['mtu'] != _rc_mtu_s:
+                        _rc_mtu_s = str(_rc_mtu) if _rc_mtu else '(-)'
+                        # check if this is a VIP address
+                        # no checks needed if yes.
+                        if _host['vip'] != _ip_str:
+                            if _rc_mtu:
+                                # if there is an MTU value, match it
+                                if _host['mtu'] != _rc_mtu_s:
+                                    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']
+                                    )
+                            elif _host['mtu'] != '1500':
+                                # there is no MTU value in reclass
+                                # and runtime value is not default
                                 self.errors.add_error(
-                                    self.errors.NET_MTU_MISMATCH,
+                                    self.errors.NET_MTU_EMPTY,
                                     host=hostname,
                                     if_name=_name,
                                     if_cidr=_ip_str,
-                                    reclass_mtu=_rc_mtu,
-                                    runtime_mtu=_host['mtu']
+                                    if_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']
-                            )
+                            # this is a VIP
+                            _name = " "*20
+                            _ip_str += " VIP"
+                            _enabled = "(-)"
+                            _r_gate = "-"
 
                         _text = "{0:25} {1:19} {2:5}{3:10} {4:4}{5:10} " \
                                 "{6} / {7} / {8}".format(
                                     _name,
                                     _ip_str,
                                     _host['mtu'],
-                                    "("+_rc_mtu_s+")" if _rc_mtu else "(No!)",
+                                    _rc_mtu_s,
                                     _host['state'],
                                     _enabled,
                                     _gate,
diff --git a/cfg_checker/modules/packages/__init__.py b/cfg_checker/modules/packages/__init__.py
index 5e717d6..a4a81bd 100644
--- a/cfg_checker/modules/packages/__init__.py
+++ b/cfg_checker/modules/packages/__init__.py
@@ -2,6 +2,35 @@
 
 import checker
 
+command_help = "Package versions check (Candidate vs Installed)"
+
+
+def init_parser(_parser):
+    # packages subparser
+    pkg_subparsers = _parser.add_subparsers(dest='type')
+
+    pkg_report_parser = pkg_subparsers.add_parser(
+        'report',
+        help="Report package versions to HTML file"
+    )
+    pkg_report_parser.add_argument(
+        '--full',
+        action="store_true", default=False,
+        help="HTML report will have all of the packages, not just errors"
+    )
+    pkg_report_parser.add_argument(
+        '--html',
+        metavar='packages_html_filename',
+        help="HTML filename to save report"
+    )
+    pkg_report_parser.add_argument(
+        '--csv',
+        metavar='packages_csv_filename',
+        help="CSV filename to save report"
+    )
+
+    return _parser
+
 
 def do_report(args):
     """Create package versions report, HTML
@@ -9,7 +38,7 @@
     :args: - parser arguments
     :return: - no return value
     """
-    _type, _filename = args_utils.get_report_type_and_filename(args)
+    _type, _filename = args_utils.get_package_report_type_and_filename(args)
 
     # init connection to salt and collect minion data
     pChecker = checker.CloudPackageChecker()
diff --git a/cfg_checker/modules/reclass/__init__.py b/cfg_checker/modules/reclass/__init__.py
index adae6df..4b8b667 100644
--- a/cfg_checker/modules/reclass/__init__.py
+++ b/cfg_checker/modules/reclass/__init__.py
@@ -8,6 +8,45 @@
 
 import validator
 
+command_help = "Reclass related checks and reports"
+
+
+def init_parser(_parser):
+    # reclass subparsers
+    reclass_subparsers = _parser.add_subparsers(dest='type')
+    reclass_list_parser = reclass_subparsers.add_parser(
+        'list',
+        help="List models available to compare"
+    )
+    reclass_list_parser.add_argument(
+        "-p",
+        "--models-path",
+        default="/srv/salt/",
+        help="Global path to search models in"
+    )
+
+    reclass_diff_parser = reclass_subparsers.add_parser(
+        'diff',
+        help="List models available to compare"
+    )
+    reclass_diff_parser.add_argument(
+        "--model1",
+        required=True,
+        help="Model A <path>. Model name is the folder name"
+    )
+    reclass_diff_parser.add_argument(
+        "--model2",
+        required=True,
+        help="Model B <path>. Model name is the folder name"
+    )
+    reclass_diff_parser.add_argument(
+        '--html',
+        metavar='reclass_html_filename',
+        help="HTML filename to save report"
+    )
+
+    return _parser
+
 
 def do_list(args):
     logger_cli.info("# Reclass list")