From d6b515f7e08989ff9b5af99659d52fa5c7934c13 Mon Sep 17 00:00:00 2001 From: Erik Lamers Date: Mon, 24 May 2021 14:01:55 +0200 Subject: [PATCH] Move argeparser to separate file Cleaner entrypoint. Also, add more verbosity options to argeparser. --- vnet_manager/argeparser.py | 58 +++++++++++++++++++++++++ vnet_manager/log.py | 18 +++++++- vnet_manager/settings/base.py | 1 + vnet_manager/tests/test_argeparser.py | 44 +++++++++++++++++++ vnet_manager/tests/test_log.py | 24 +++++++++- vnet_manager/tests/test_vnet_manager.py | 41 +---------------- vnet_manager/vnet_manager.py | 51 +++------------------- 7 files changed, 150 insertions(+), 87 deletions(-) create mode 100644 vnet_manager/argeparser.py create mode 100644 vnet_manager/tests/test_argeparser.py diff --git a/vnet_manager/argeparser.py b/vnet_manager/argeparser.py new file mode 100644 index 0000000..d0de78d --- /dev/null +++ b/vnet_manager/argeparser.py @@ -0,0 +1,58 @@ +from argparse import Namespace, ArgumentParser +from typing import Sequence + +from vnet_manager.conf import settings + + +def parse_vnet_args(args: Sequence = None) -> Namespace: + parser = ArgumentParser(description="VNet-manager a virtual network manager - manages containers to create virtual networks") + parser.add_argument( + "action", + choices=sorted(settings.VALID_ACTIONS), + help="The action to preform on the virtual network, use ' help' for information about that action", + ) + parser.add_argument("config", help="The yaml config file to use", nargs="?", default="default") + + # Options + parser.add_argument( + "-m", + "--machines", + nargs="*", + help="Just apply the actions on the following machine names " "(default is all machines defined in the config file)", + ) + parser.add_argument("-y", "--yes", action="store_true", help="Answer yes to all questions") + parser.add_argument("-nh", "--no-hosts", action="store_true", help="Disable creation of /etc/hosts") + + start_group = parser.add_argument_group("Start options", "These options can be specified for the start action") + start_group.add_argument("-s", "--sniffer", action="store_true", help="Start a TCPdump sniffer on the VNet interfaces") + + destroy_group = parser.add_argument_group("Destroy options", "These options can be specified for the destroy action") + destroy_group.add_argument("-b", "--base-image", action="store_true", help="Destroy the base image instead of the machines") + + logging_group = parser.add_argument_group("Verbosity options", "Control output verbosity (can be supplied multiple times)") + logging_group.add_argument("-v", "--verbose", action="count", default=0, help="Be more verbose") + logging_group.add_argument("-q", "--quite", action="count", default=0, help="Be more quite") + return validate_argument_sanity(parser.parse_args(args=args), parser) + + +def validate_argument_sanity(args: Namespace, parser: ArgumentParser) -> Namespace: + """ + Validates the passed arguments for sanity + :param args: Namespace, The already processed user arguments + :param parser: ArgumentParser, The parser object + :return: Namespace, The validated arguments + :raises: SystemExit, if arguments are not sane + """ + # User input sanity checks + if args.action == "status": + # For people who are used to status calls + args.action = "show" + if args.config == "default" and args.action in settings.CONFIG_REQUIRED_ACTIONS: + parser.error("This action requires a config file to be passed") + if args.sniffer and not args.action == "start": + parser.error("The sniffer option only makes sense with the 'start' action") + if args.base_image and not args.action == "destroy": + parser.error("The base_image option only makes sense with the 'destroy' action") + if args.no_hosts and not args.action == "create": + parser.error("The no_hosts option only makes sense with the 'create' action") + return args diff --git a/vnet_manager/log.py b/vnet_manager/log.py index e8cb816..cf55c8e 100644 --- a/vnet_manager/log.py +++ b/vnet_manager/log.py @@ -1,10 +1,26 @@ import logging import logging.config +from argparse import Namespace from vnet_manager.conf import settings -def setup_console_logging(verbosity: int = logging.INFO): +def get_logging_verbosity(args: Namespace, default_verbosity: int = settings.LOGGING_DEFAULT_VERBOSITY) -> int: + """ + Get the logging verbosity based upon any passed verbosity arguments + :param args: Namespace, the parsed command line arguments + :param default_verbosity: int, the initial verbosity to manipulate + :return: int: The logging verbosity setting + """ + logging_verbs = {0: logging.CRITICAL, 1: logging.ERROR, 2: logging.WARNING, 3: logging.INFO, 4: logging.DEBUG} + # Compare the amount of verbosity args against the default verbosity + print(default_verbosity, args) + mod = default_verbosity + args.verbose - args.quite + # Return the appropriate logging level (must be within the defined logging levels) + return logging_verbs[max(min(len(logging_verbs) - 1, mod), 0)] + + +def setup_console_logging(verbosity: int = logging.INFO) -> None: """ :param int verbosity: Verbosity level logging. """ diff --git a/vnet_manager/settings/base.py b/vnet_manager/settings/base.py index 930e967..b4d82f4 100644 --- a/vnet_manager/settings/base.py +++ b/vnet_manager/settings/base.py @@ -47,6 +47,7 @@ "pyroute2.ndb": {"level": "WARNING"}, }, } +LOGGING_DEFAULT_VERBOSITY = 3 # logging.INFO # VNet Manager static settings / config # provider config diff --git a/vnet_manager/tests/test_argeparser.py b/vnet_manager/tests/test_argeparser.py new file mode 100644 index 0000000..5a2e874 --- /dev/null +++ b/vnet_manager/tests/test_argeparser.py @@ -0,0 +1,44 @@ +from io import StringIO +from unittest.mock import patch + +from vnet_manager.argeparser import parse_vnet_args +from vnet_manager.tests import VNetTestCase + + +default_args = ["list", "config"] + + +class TestParseArgs(VNetTestCase): + def test_parse_args_produces_known_args(self): + known_args = ("action", "config", "machines", "yes", "verbose", "sniffer", "base_image", "no_hosts") + args = parse_vnet_args(default_args) + for arg in known_args: + self.assertTrue(hasattr(args, arg), msg="Argument {} not found in parse_args return value".format(arg)) + + @patch("sys.stderr", new_callable=StringIO) + def test_parse_args_exists_when_config_required_action_without_config_is_passed(self, stderr): + with self.assertRaises(SystemExit): + parse_vnet_args(["create"]) + self.assertTrue(stderr.getvalue().strip().endswith("This action requires a config file to be passed")) + + @patch("sys.stderr", new_callable=StringIO) + def test_parse_args_exists_when_sniffer_is_passed_without_start_action(self, stderr): + with self.assertRaises(SystemExit): + parse_vnet_args(["list", "config", "--sniffer"]) + self.assertTrue(stderr.getvalue().strip().endswith("The sniffer option only makes sense with the 'start' action")) + + @patch("sys.stderr", new_callable=StringIO) + def test_parse_args_exists_when_base_image_passed_without_destroy_action(self, stderr): + with self.assertRaises(SystemExit): + parse_vnet_args(["list", "config", "--base-image"]) + self.assertTrue(stderr.getvalue().strip().endswith("The base_image option only makes sense with the 'destroy' action")) + + @patch("sys.stderr", new_callable=StringIO) + def test_parse_args_exists_when_no_hosts_passed_without_create_action(self, stderr): + with self.assertRaises(SystemExit): + parse_vnet_args(["list", "config", "--no-hosts"]) + self.assertTrue(stderr.getvalue().strip().endswith("The no_hosts option only makes sense with the 'create' action")) + + def test_parse_args_sets_show_action_on_status_action(self): + args = parse_vnet_args(["status", "config"]) + self.assertEqual(args.action, "show") diff --git a/vnet_manager/tests/test_log.py b/vnet_manager/tests/test_log.py index 3adc0d8..b693988 100644 --- a/vnet_manager/tests/test_log.py +++ b/vnet_manager/tests/test_log.py @@ -1,10 +1,32 @@ import logging +from argparse import ArgumentParser -from vnet_manager.log import setup_console_logging +from vnet_manager.log import setup_console_logging, get_logging_verbosity from vnet_manager.tests import VNetTestCase from vnet_manager.conf import settings +def my_test_args(args): + parser = ArgumentParser() + parser.add_argument("-q", "--quite", action="count", default=0) + parser.add_argument("-v", "--verbose", action="count", default=0) + return parser.parse_args(args=args) + + +class TestGetLoggingVerbosity(VNetTestCase): + def test_get_logging_verbosity_never_returns_higher_number_then_defined_verbs(self): + args = my_test_args(["-vvvvvvvvvvv", "--verbose", "-v"]) + self.assertEqual(get_logging_verbosity(args), logging.DEBUG) + + def test_get_logging_verbosity_never_returns_lower_number_then_defined_verbs(self): + args = my_test_args(["-qqqqqqqqqqq", "--quite", "-q"]) + self.assertEqual(get_logging_verbosity(args), logging.CRITICAL) + + def test_get_logging_verbosity_return_error_level_if_two_quites_passed(self): + args = my_test_args(["--quite", "-q"]) + self.assertEqual(get_logging_verbosity(args), logging.ERROR) + + class TestLog(VNetTestCase): def test_setup_console_logging_sets_up_INFO_logging_by_default(self): setup_console_logging() diff --git a/vnet_manager/tests/test_vnet_manager.py b/vnet_manager/tests/test_vnet_manager.py index abd142b..9911c07 100644 --- a/vnet_manager/tests/test_vnet_manager.py +++ b/vnet_manager/tests/test_vnet_manager.py @@ -1,51 +1,14 @@ -from unittest.mock import patch, Mock -from io import StringIO +from unittest.mock import Mock from os import environ, EX_NOPERM from logging import INFO, DEBUG from vnet_manager.tests import VNetTestCase from vnet_manager.conf import settings -from vnet_manager.vnet_manager import parse_args, main +from vnet_manager.vnet_manager import main default_args = ["list", "config"] -class TestParseArgs(VNetTestCase): - def test_parse_args_produces_known_args(self): - known_args = ("action", "config", "machines", "yes", "verbose", "sniffer", "base_image", "no_hosts") - args = parse_args(default_args) - for arg in known_args: - self.assertTrue(hasattr(args, arg), msg="Argument {} not found in parse_args return value".format(arg)) - - @patch("sys.stderr", new_callable=StringIO) - def test_parse_args_exists_when_config_required_action_without_config_is_passed(self, stderr): - with self.assertRaises(SystemExit): - parse_args(["create"]) - self.assertTrue(stderr.getvalue().strip().endswith("This action requires a config file to be passed")) - - @patch("sys.stderr", new_callable=StringIO) - def test_parse_args_exists_when_sniffer_is_passed_without_start_action(self, stderr): - with self.assertRaises(SystemExit): - parse_args(["list", "config", "--sniffer"]) - self.assertTrue(stderr.getvalue().strip().endswith("The sniffer option only makes sense with the 'start' action")) - - @patch("sys.stderr", new_callable=StringIO) - def test_parse_args_exists_when_base_image_passed_without_destroy_action(self, stderr): - with self.assertRaises(SystemExit): - parse_args(["list", "config", "--base-image"]) - self.assertTrue(stderr.getvalue().strip().endswith("The base_image option only makes sense with the 'destroy' action")) - - @patch("sys.stderr", new_callable=StringIO) - def test_parse_args_exists_when_no_hosts_passed_without_create_action(self, stderr): - with self.assertRaises(SystemExit): - parse_args(["list", "config", "--no-hosts"]) - self.assertTrue(stderr.getvalue().strip().endswith("The no_hosts option only makes sense with the 'create' action")) - - def test_parse_args_sets_show_action_on_status_action(self): - args = parse_args(["status", "config"]) - self.assertEqual(args.action, "show") - - class TestVNetManagerMain(VNetTestCase): def setUp(self) -> None: self.setup_console_logging = self.set_up_patch("vnet_manager.vnet_manager.setup_console_logging") diff --git a/vnet_manager/vnet_manager.py b/vnet_manager/vnet_manager.py index ed339f9..b1f2eec 100644 --- a/vnet_manager/vnet_manager.py +++ b/vnet_manager/vnet_manager.py @@ -1,69 +1,28 @@ import sys -from argparse import ArgumentParser, Namespace -from logging import INFO, DEBUG, getLogger +from logging import getLogger from os import EX_NOPERM, environ from typing import Sequence from vnet_manager.conf import settings -from vnet_manager.log import setup_console_logging +from vnet_manager.log import setup_console_logging, get_logging_verbosity from vnet_manager.actions.manager import ActionManager from vnet_manager.utils.user import check_for_root_user +from vnet_manager.argeparser import parse_vnet_args logger = getLogger(__name__) -def parse_args(args: Sequence = None) -> Namespace: - parser = ArgumentParser(description="VNet-manager a virtual network manager - manages containers to create virtual networks") - parser.add_argument( - "action", - choices=sorted(settings.VALID_ACTIONS), - help="The action to preform on the virtual network, use ' help' for information about that action", - ) - parser.add_argument("config", help="The yaml config file to use", nargs="?", default="default") - - # Options - parser.add_argument( - "-m", - "--machines", - nargs="*", - help="Just apply the actions on the following machine names " "(default is all machines defined in the config file)", - ) - parser.add_argument("-y", "--yes", action="store_true", help="Answer yes to all questions") - parser.add_argument("-v", "--verbose", action="store_true", help="Print debug messages") - parser.add_argument("-nh", "--no-hosts", action="store_true", help="Disable creation of /etc/hosts") - - start_group = parser.add_argument_group("Start options", "These options can be specified for the start action") - start_group.add_argument("-s", "--sniffer", action="store_true", help="Start a TCPdump sniffer on the VNet interfaces") - destroy_group = parser.add_argument_group("Destroy options", "These options can be specified for the destroy action") - destroy_group.add_argument("-b", "--base-image", action="store_true", help="Destroy the base image instead of the machines") - args = parser.parse_args(args=args) - - # User input sanity checks - if args.action == "status": - # For people who are used to status calls - args.action = "show" - if args.config == "default" and args.action in settings.CONFIG_REQUIRED_ACTIONS: - parser.error("This action requires a config file to be passed") - if args.sniffer and not args.action == "start": - parser.error("The sniffer option only makes sense with the 'start' action") - if args.base_image and not args.action == "destroy": - parser.error("The base_image option only makes sense with the 'destroy' action") - if args.no_hosts and not args.action == "create": - parser.error("The no_hosts option only makes sense with the 'create' action") - return args - - def main(args: Sequence = None) -> int: """ Program entry point :param list args: The pre-cooked arguments to pass to the ArgParser :return int: exit_code """ - args = parse_args(args) + args = parse_vnet_args(args) # Set the VNET_FORCE variable, if --yes is given this will answer yes to all questions environ[settings.VNET_FORCE_ENV_VAR] = "true" if args.yes else "false" # Setup logging - setup_console_logging(verbosity=DEBUG if args.verbose else INFO) + setup_console_logging(verbosity=get_logging_verbosity(args)) # Most VNet operation require root. So, do a root check if not check_for_root_user(): logger.critical("This program should only be run as root")