diff --git a/doc/source/cli.rst b/doc/source/cli.rst index c41b1d85..572fe9c2 100644 --- a/doc/source/cli.rst +++ b/doc/source/cli.rst @@ -20,3 +20,4 @@ Command Options ~~~~~~~~~~~~~~~ .. autoprogram-cliff:: validation.cli + :application: validation diff --git a/setup.cfg b/setup.cfg index 75e6a0aa..ab3ef2da 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,10 @@ classifier = [files] packages = validations_libs +data_files = + etc = + validation.cfg + [compile_catalog] directory = validations-libs/locale domain = validations-lib diff --git a/validation.cfg b/validation.cfg new file mode 100644 index 00000000..48801496 --- /dev/null +++ b/validation.cfg @@ -0,0 +1,63 @@ +[default] +# Default configuration for the Validation Framework +# These are mainly CLI parameters which can be set here in order to avoid +# to provide the same parameters on each runs. + +# Location where the Validation playbooks are stored. +validation_dir = /usr/share/ansible/validation-playbooks + +# Path where the framework is supposed to write logs and results. +# Note: this should not be a relative path. +# By default the framework log in $HOME/validations. +# Uncomment this line according to your prefered location: +# validation_log_dir = /usr/share/validations + +# Location where the Ansible Validation Callback, Libraries and Modules are +# stored. +ansible_base_dir = /usr/share/ansible/ + +# Ssh user for the remote access +#ssh_user = stack + +# Output log for the Validation results. +output_log = output.log + +# Limitation of the number of results to return to the console. +history_limit = 15 + +fit_width = True + +[ansible_runner] +# Ansible Runner configuration parameters. +# Here you can set the Runner parameters which will be used by the framework. +# Note that only those parameters are supported, any other custom parameters +# will be ignored. + +# Verbosity for Ansible +verbosity = 5 + +# Fact cache directory location and type +# fact_cache = /var/log/validations/artifacts/ +fact_cache_type = jsonfile + +# Inventory for Ansible +#inventory = hosts.yaml + +quiet = True +rotate_artifacts = 256 + +[ansible_environment] +# Ansible Environment variables. +# You can provide here, all the Ansible configuration variables documented here: +# https://docs.ansible.com/ansible/latest/reference_appendices/config.html + +# Here is a set of parameters used by the Validation Framework as example: +#ANSIBLE_LOG_PATH = /home/stack/ansible.log +#ANSIBLE_REMOTE_USER = stack +ANSIBLE_CALLBACK_WHITELIST = validation_stdout,validation_json,profile_tasks +ANSIBLE_STDOUT_CALLBACK = validation_stdout + +# Callback settings which are part of Ansible environment variables. +# Configuration for HTTP Server callback +HTTP_JSON_SERVER = http://localhost +HTTP_JSON_PORT = 8080 diff --git a/validations_libs/ansible.py b/validations_libs/ansible.py index de252051..4b1ef1fd 100644 --- a/validations_libs/ansible.py +++ b/validations_libs/ansible.py @@ -26,6 +26,7 @@ import yaml from six.moves import configparser from validations_libs import constants +from validations_libs import utils LOG = logging.getLogger(__name__ + ".ansible") @@ -275,6 +276,17 @@ class Ansible(object): else: return env + def _dump_validation_config(self, config, path, filename='validation.cfg'): + """Dump Validation config in artifact directory""" + parser = configparser.ConfigParser() + for section_key in config.keys(): + parser.add_section(section_key) + for item_key in config[section_key].keys(): + parser.set(section_key, item_key, + str(config[section_key][item_key])) + with open('{}/{}'.format(path, filename), 'w') as conf: + parser.write(conf) + def run(self, playbook, inventory, workdir, playbook_dir=None, connection='smart', output_callback=None, base_dir=constants.DEFAULT_VALIDATIONS_BASEDIR, @@ -283,9 +295,10 @@ class Ansible(object): verbosity=0, quiet=False, extra_vars=None, gathering_policy='smart', extra_env_variables=None, parallel_run=False, - callback_whitelist=None, ansible_cfg=None, + callback_whitelist=None, ansible_cfg_file=None, ansible_timeout=30, ansible_artifact_path=None, - log_path=None, run_async=False, python_interpreter=None): + log_path=None, run_async=False, python_interpreter=None, + validation_cfg_file=None): """Execute one or multiple Ansible playbooks :param playbook: The Absolute path of the Ansible playbook @@ -341,9 +354,10 @@ class Ansible(object): Custom output_callback is also whitelisted. (Defaults to ``None``) :type callback_whitelist: ``list`` or ``string`` - :param ansible_cfg: Path to an ansible configuration file. One will be - generated in the artifact path if this option is None. - :type ansible_cfg: ``string`` + :param ansible_cfg_file: Path to an ansible configuration file. One + will be generated in the artifact path if + this option is None. + :type ansible_cfg_file: ``string`` :param ansible_timeout: Timeout for ansible connections. (Defaults to ``30 minutes``) :type ansible_timeout: ``integer`` @@ -360,6 +374,10 @@ class Ansible(object): ``auto_silent`` or the default one ``auto_legacy``) :type python_interpreter: ``string`` + :param validation_cfg_file: A dictionary of configuration for + Validation loaded from an validation.cfg + file. + :type validation_cfg_file: ``dict`` :return: A ``tuple`` containing the the absolute path of the executed playbook, the return code and the status of the run @@ -405,16 +423,17 @@ class Ansible(object): ansible_timeout, callback_whitelist, base_dir, python_interpreter)) - if 'ANSIBLE_CONFIG' not in env and not ansible_cfg: - ansible_cfg = os.path.join(ansible_artifact_path, 'ansible.cfg') - config = configparser.ConfigParser() - config.add_section('defaults') - config.set('defaults', 'internal_poll_interval', '0.05') - with open(ansible_cfg, 'w') as f: - config.write(f) - env['ANSIBLE_CONFIG'] = ansible_cfg - elif 'ANSIBLE_CONFIG' not in env and ansible_cfg: - env['ANSIBLE_CONFIG'] = ansible_cfg + if 'ANSIBLE_CONFIG' not in env and not ansible_cfg_file: + ansible_cfg_file = os.path.join(ansible_artifact_path, + 'ansible.cfg') + ansible_config = configparser.ConfigParser() + ansible_config.add_section('defaults') + ansible_config.set('defaults', 'internal_poll_interval', '0.05') + with open(ansible_cfg_file, 'w') as f: + ansible_config.write(f) + env['ANSIBLE_CONFIG'] = ansible_cfg_file + elif 'ANSIBLE_CONFIG' not in env and ansible_cfg_file: + env['ANSIBLE_CONFIG'] = ansible_cfg_file if log_path: env['VALIDATIONS_LOG_DIR'] = log_path @@ -434,7 +453,6 @@ class Ansible(object): if not BACKWARD_COMPAT: r_opts.update({ - 'envvars': envvars, 'project_dir': playbook_dir, 'fact_cache': ansible_artifact_path, 'fact_cache_type': 'jsonfile' @@ -453,6 +471,17 @@ class Ansible(object): if parallel_run: r_opts['directory_isolation_base_path'] = ansible_artifact_path + + if validation_cfg_file: + if 'ansible_runner' in validation_cfg_file.keys(): + r_opts.update(validation_cfg_file['ansible_runner']) + if 'ansible_environment' in validation_cfg_file.keys(): + envvars.update(validation_cfg_file['ansible_environment']) + self._dump_validation_config(validation_cfg_file, + ansible_artifact_path) + if not BACKWARD_COMPAT: + r_opts.update({'envvars': envvars}) + runner_config = ansible_runner.runner_config.RunnerConfig(**r_opts) runner_config.prepare() runner_config.env['ANSIBLE_STDOUT_CALLBACK'] = \ diff --git a/validations_libs/cli/app.py b/validations_libs/cli/app.py index 1206fe50..a646be74 100644 --- a/validations_libs/cli/app.py +++ b/validations_libs/cli/app.py @@ -13,7 +13,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - import sys from cliff.app import App diff --git a/validations_libs/cli/base.py b/validations_libs/cli/base.py index 1b954151..8f983a29 100644 --- a/validations_libs/cli/base.py +++ b/validations_libs/cli/base.py @@ -14,10 +14,15 @@ # License for the specific language governing permissions and limitations # under the License. +import os from cliff import _argparse from cliff.command import Command from cliff.lister import Lister +from cliff.show import ShowOne + +from validations_libs import constants +from validations_libs import utils # Handle backward compatibility for Cliff 2.16.0 in stable/train: if hasattr(_argparse, 'SmartHelpFormatter'): @@ -26,11 +31,50 @@ else: from cliff.command import _SmartHelpFormatter as SmartHelpFormatter +class Base: + """Base class for CLI arguments management""" + config = {} + + def _format_arg(self, parser): + """Format arguments parser""" + namespace, argv = parser.parse_known_args() + return [arg.lstrip(parser.prefix_chars).replace('-', '_') + for arg in argv] + + def set_argument_parser(self, parser, section='default'): + """ Set Arguments parser depending of the precedence ordering: + * User CLI arguments + * Configuration file + * Default CLI values + """ + cli_args = self._format_arg(parser) + args, _ = parser.parse_known_args() + self.config = utils.load_config(os.path.abspath(args.config)) + config_args = self.config.get(section, {}) + for key, value in args._get_kwargs(): + # matbu: manage the race when user's cli arg is the same than + # the parser default value. The user's cli arg will *always* + # takes precedence on others. + if parser.get_default(key) == value and key in cli_args: + try: + cli_value = cli_args[cli_args.index(key)+1] + config_args.update({key: cli_value}) + except KeyError: + print('Key not found in cli: {}').format(key) + elif parser.get_default(key) != value: + config_args.update({key: value}) + elif key not in config_args.keys(): + config_args.update({key: value}) + parser.set_defaults(**config_args) + return parser + + class BaseCommand(Command): """Base Command client implementation class""" def get_parser(self, prog_name): """Argument parser for base command""" + self.base = Base() parser = _argparse.ArgumentParser( description=self.get_description(), epilog=self.get_epilog(), @@ -40,6 +84,14 @@ class BaseCommand(Command): ) for hook in self._hooks: hook.obj.get_parser(parser) + + parser.add_argument( + '--config', + dest='config', + default=utils.find_config_file(), + help=("Config file path for Validation.") + ) + return parser @@ -49,6 +101,7 @@ class BaseLister(Lister): def get_parser(self, prog_name): """Argument parser for base lister""" parser = super(BaseLister, self).get_parser(prog_name) + self.base = Base() vf_parser = _argparse.ArgumentParser( description=self.get_description(), epilog=self.get_epilog(), @@ -60,4 +113,28 @@ class BaseLister(Lister): for action in parser._actions: vf_parser._add_action(action) + vf_parser.add_argument( + '--config', + dest='config', + default=utils.find_config_file(), + help=("Config file path for Validation.") + ) + return vf_parser + + +class BaseShow(ShowOne): + """Base Show client implementation class""" + + def get_parser(self, parser): + """Argument parser for base show""" + parser = super(BaseShow, self).get_parser(parser) + self.base = Base() + parser.add_argument( + '--config', + dest='config', + default=utils.find_config_file(), + help=("Config file path for Validation.") + ) + + return parser diff --git a/validations_libs/cli/history.py b/validations_libs/cli/history.py index ff9dd866..acf5db1d 100644 --- a/validations_libs/cli/history.py +++ b/validations_libs/cli/history.py @@ -19,8 +19,7 @@ import json from validations_libs import constants from validations_libs.validation_actions import ValidationActions from validations_libs.validation_logs import ValidationLogs -from validations_libs.cli.base import BaseCommand -from validations_libs.cli.base import BaseLister +from validations_libs.cli.base import BaseCommand, BaseLister class ListHistory(BaseLister): @@ -46,28 +45,27 @@ class ListHistory(BaseLister): default=constants.VALIDATIONS_LOG_BASEDIR, help=("Path where the validation log files " "is located.")) - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): + validation_log_dir = parsed_args.validation_log_dir + history_limit = parsed_args.history_limit - if parsed_args.history_limit < 1: - raise ValueError( - ( - "Number of the most recent runs must be > 0. " - "You have provided {}").format( - parsed_args.history_limit)) + if history_limit < 1: + msg = ("Number of the most recent runs must be > 0. " + "You have provided {}").format(history_limit) + raise ValueError(msg) self.app.LOG.info( - ( - "Limiting output to the maximum of " - "{} last validations.").format( - parsed_args.history_limit)) + ("Limiting output to the maximum of " + "{} last validations.").format(history_limit)) actions = ValidationActions() return actions.show_history( validation_ids=parsed_args.validation, log_path=parsed_args.validation_log_dir, - history_limit=parsed_args.history_limit) + history_limit=history_limit) class GetHistory(BaseCommand): @@ -88,10 +86,10 @@ class GetHistory(BaseCommand): default=constants.VALIDATIONS_LOG_BASEDIR, help=("Path where the validation log files " "is located.")) - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): - self.app.LOG.debug( ( "Obtaining information about the validation run {}\n" diff --git a/validations_libs/cli/lister.py b/validations_libs/cli/lister.py index 07a835b3..7ca32683 100644 --- a/validations_libs/cli/lister.py +++ b/validations_libs/cli/lister.py @@ -14,14 +14,13 @@ # License for the specific language governing permissions and limitations # under the License. -from cliff.lister import Lister - from validations_libs.validation_actions import ValidationActions from validations_libs import constants +from validations_libs.cli.base import BaseLister from validations_libs.cli.parseractions import CommaListAction -class ValidationList(Lister): +class ValidationList(BaseLister): """List the Validations Catalog""" def get_parser(self, parser): @@ -52,7 +51,8 @@ class ValidationList(Lister): default=constants.ANSIBLE_VALIDATION_DIR, help=("Path where the validation playbooks " "are located.")) - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): """Take validation action""" @@ -61,6 +61,7 @@ class ValidationList(Lister): category = parsed_args.category product = parsed_args.product validation_dir = parsed_args.validation_dir + group = parsed_args.group v_actions = ValidationActions(validation_path=validation_dir) return (v_actions.list_validations(groups=group, diff --git a/validations_libs/cli/run.py b/validations_libs/cli/run.py index a4e03371..ebc5161b 100644 --- a/validations_libs/cli/run.py +++ b/validations_libs/cli/run.py @@ -156,17 +156,23 @@ class Run(BaseCommand): "if more than one product is required " "separate the product names with commas.")) + if self.app: + # Merge config and CLI args: + return self.base.set_argument_parser(parser) return parser def take_action(self, parsed_args): """Take validation action""" - v_actions = ValidationActions( - validation_path=parsed_args.validation_dir) + # Get config: + config = self.base.config + v_actions = ValidationActions(parsed_args.validation_dir) # Ansible execution should be quiet while using the validations_json # default callback and be verbose while passing ANSIBLE_SDTOUT_CALLBACK # environment variable to Ansible through the --extra-env-vars argument - quiet_mode = True + runner_config = (config.get('ansible_runner', {}) + if isinstance(config, dict) else {}) + quiet_mode = runner_config.get('quiet', True) extra_env_vars = parsed_args.extra_env_vars if extra_env_vars: if "ANSIBLE_STDOUT_CALLBACK" in extra_env_vars.keys(): @@ -178,7 +184,8 @@ class Run(BaseCommand): "Loading extra vars file {}".format( parsed_args.extra_vars_file)) - extra_vars = common.read_extra_vars_file(parsed_args.extra_vars_file) + extra_vars = common.read_extra_vars_file( + parsed_args.extra_vars_file) try: results = v_actions.run_validations( @@ -195,8 +202,8 @@ class Run(BaseCommand): python_interpreter=parsed_args.python_interpreter, quiet=quiet_mode, ssh_user=parsed_args.ssh_user, - log_path=parsed_args.validation_log_dir - ) + log_path=parsed_args.validation_log_dir, + validation_config=config) except RuntimeError as e: raise RuntimeError(e) diff --git a/validations_libs/cli/show.py b/validations_libs/cli/show.py index 20abdc59..1d4c6703 100644 --- a/validations_libs/cli/show.py +++ b/validations_libs/cli/show.py @@ -14,15 +14,13 @@ # License for the specific language governing permissions and limitations # under the License. -from cliff.show import ShowOne -from cliff.lister import Lister - from validations_libs.validation_actions import ValidationActions from validations_libs import constants from validations_libs.cli.parseractions import CommaListAction +from validations_libs.cli.base import BaseShow, BaseLister -class Show(ShowOne): +class Show(BaseShow): """Show detailed informations about a Validation""" def get_parser(self, parser): @@ -36,7 +34,8 @@ class Show(ShowOne): metavar="", type=str, help="Show a specific validation.") - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): """Take validation action""" @@ -51,7 +50,7 @@ class Show(ShowOne): return data.keys(), data.values() -class ShowGroup(Lister): +class ShowGroup(BaseLister): """Show detailed informations about Validation Groups""" def get_parser(self, parser): @@ -63,7 +62,8 @@ class ShowGroup(Lister): help=("Path where the validation playbooks " "are located.")) - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): """Take validation action""" @@ -72,7 +72,7 @@ class ShowGroup(Lister): return v_actions.group_information(constants.VALIDATION_GROUPS_INFO) -class ShowParameter(ShowOne): +class ShowParameter(BaseShow): """Show Validation(s) parameter(s) Display Validation(s) Parameter(s) which could be overriden during an @@ -147,11 +147,13 @@ class ShowParameter(ShowOne): help=("Print representation of the validation. " "The choices of the output format is json,yaml. ") ) - - return parser + # Merge config and CLI args: + return self.base.set_argument_parser(parser) def take_action(self, parsed_args): - v_actions = ValidationActions(parsed_args.validation_dir) + + validation_dir = parsed_args.validation_dir + v_actions = ValidationActions(validation_dir) params = v_actions.show_validations_parameters( validations=parsed_args.validation_name, groups=parsed_args.group, diff --git a/validations_libs/constants.py b/validations_libs/constants.py index f156b4b2..1260b99f 100644 --- a/validations_libs/constants.py +++ b/validations_libs/constants.py @@ -38,3 +38,7 @@ VALIDATIONS_LOG_BASEDIR = os.path.expanduser('~/validations') VALIDATION_ANSIBLE_ARTIFACT_PATH = os.path.join( VALIDATIONS_LOG_BASEDIR, 'artifacts') + +ANSIBLE_RUNNER_CONFIG_PARAMETERS = ['verbosity', 'extravars', 'fact_cache', + 'fact_cache_type', 'inventory', 'playbook', + 'project_dir', 'quiet', 'rotate_artifacts'] diff --git a/validations_libs/tests/cli/fakes.py b/validations_libs/tests/cli/fakes.py index 02df08f7..5895d45a 100644 --- a/validations_libs/tests/cli/fakes.py +++ b/validations_libs/tests/cli/fakes.py @@ -12,17 +12,26 @@ # License for the specific language governing permissions and limitations # under the License. # +import sys from unittest import TestCase from validations_libs.cli import app +from validations_libs.cli import base +from validations_libs import utils + + +try: + from unittest import mock +except ImportError: + import mock class BaseCommand(TestCase): def check_parser(self, cmd, args, verify_args): - cmd_parser = cmd.get_parser('check_parser') try: + cmd_parser = cmd.get_parser('check_parser') parsed_args = cmd_parser.parse_args(args) except SystemExit: raise Exception("Argument parse failed") @@ -35,8 +44,15 @@ class BaseCommand(TestCase): def setUp(self): super(BaseCommand, self).setUp() + self._set_args([]) self.app = app.ValidationCliApp() + def _set_args(self, args): + sys.argv = sys.argv[:1] + sys.argv.extend(args) + return args + + KEYVALUEACTION_VALUES = { 'valid': 'foo=bar', 'invalid_noeq': 'foo>bar', diff --git a/validations_libs/tests/cli/test_app.py b/validations_libs/tests/cli/test_app.py new file mode 100644 index 00000000..fc70b509 --- /dev/null +++ b/validations_libs/tests/cli/test_app.py @@ -0,0 +1,115 @@ +# Copyright 2021 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import sys + +try: + from unittest import mock +except ImportError: + import mock +from unittest import TestCase + +from validations_libs.cli import app +from validations_libs.cli import lister +from validations_libs.cli import history + + +class TestArgApp(TestCase): + + def setUp(self): + super(TestArgApp, self).setUp() + self._set_args([]) + self.app = app.ValidationCliApp() + + def _set_args(self, args): + sys.argv = sys.argv[:1] + sys.argv.extend(args) + return args + + def test_validation_dir_config_cli(self): + args = ['--validation-dir', 'foo'] + self._set_args(args) + cmd = lister.ValidationList(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('foo', parsed_args.validation_dir) + + @mock.patch('validations_libs.constants.ANSIBLE_VALIDATION_DIR', 'bar') + @mock.patch('validations_libs.utils.find_config_file', + return_value='validation.cfg') + def test_validation_dir_config_no_cli(self, mock_config): + args = [] + self._set_args(args) + cmd = lister.ValidationList(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('/usr/share/ansible/validation-playbooks', + parsed_args.validation_dir) + + @mock.patch('validations_libs.constants.ANSIBLE_VALIDATION_DIR', 'bar') + @mock.patch('validations_libs.utils.find_config_file', + return_value='/etc/validation.cfg') + def test_validation_dir_config_no_cli_no_config(self, mock_config): + args = [] + self._set_args(args) + cmd = lister.ValidationList(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('bar', parsed_args.validation_dir) + + @mock.patch('validations_libs.constants.ANSIBLE_VALIDATION_DIR', + '/usr/share/ansible/validation-playbooks') + @mock.patch('validations_libs.utils.find_config_file', + return_value='validation.cfg') + def test_validation_dir_config_no_cli_same_consts(self, mock_config): + args = [] + self._set_args(args) + cmd = lister.ValidationList(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('/usr/share/ansible/validation-playbooks', + parsed_args.validation_dir) + + def test_get_history_cli_arg(self): + args = ['123', '--validation-log-dir', '/foo/log/dir'] + self._set_args(args) + cmd = history.GetHistory(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('/foo/log/dir', + parsed_args.validation_log_dir) + + @mock.patch('validations_libs.utils.find_config_file', + return_value='validation.cfg') + def test_get_history_cli_arg_and_config_file(self, mock_config): + args = ['123', '--validation-log-dir', '/foo/log/dir'] + self._set_args(args) + cmd = history.GetHistory(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('/foo/log/dir', + parsed_args.validation_log_dir) + + @mock.patch('validations_libs.constants.VALIDATIONS_LOG_BASEDIR', + '/home/foo/validations') + @mock.patch('validations_libs.utils.find_config_file', + return_value='validation.cfg') + def test_get_history_no_cli_arg_and_config_file(self, mock_config): + args = ['123'] + self._set_args(args) + cmd = history.GetHistory(self.app, None) + parser = cmd.get_parser('fake') + parsed_args = parser.parse_args(args) + self.assertEqual('/home/foo/validations', + parsed_args.validation_log_dir) diff --git a/validations_libs/tests/cli/test_base.py b/validations_libs/tests/cli/test_base.py new file mode 100644 index 00000000..5136175e --- /dev/null +++ b/validations_libs/tests/cli/test_base.py @@ -0,0 +1,94 @@ +# Copyright 2021 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +try: + from unittest import mock +except ImportError: + import mock +from unittest import TestCase + +from validations_libs.cli import lister +from validations_libs.cli import base +from validations_libs.tests import fakes +from validations_libs.tests.cli.fakes import BaseCommand + +import argparse + + +class TestArgParse(argparse.ArgumentParser): + + config = 'foo' + + def __init__(self): + super(TestArgParse, self).__init__() + + +class TestBase(BaseCommand): + + def setUp(self): + super(TestBase, self).setUp() + self.cmd = lister.ValidationList(self.app, None) + self.base = base.Base() + + @mock.patch('argparse.ArgumentParser.parse_known_args', + return_value=(TestArgParse(), ['foo-bar'])) + @mock.patch('os.path.abspath', return_value='/foo') + @mock.patch('validations_libs.utils.load_config', + return_value=fakes.DEFAULT_CONFIG) + def test_config_args(self, mock_config, mock_path, mock_argv): + cmd_parser = self.cmd.get_parser('check_parser') + + self.assertEqual(['foo_bar'], self.base._format_arg(cmd_parser)) + + @mock.patch('os.path.abspath', return_value='/foo') + @mock.patch('validations_libs.utils.load_config', + return_value=fakes.DEFAULT_CONFIG) + def test_argument_parser_cli_choice(self, mock_load, mock_path): + arglist = ['--validation-dir', 'foo', '--config', 'validation.cfg'] + verifylist = [('validation_dir', 'foo')] + self._set_args(arglist) + cmd_parser = self.cmd.get_parser('check_parser') + parser = self.base.set_argument_parser(cmd_parser) + + self.assertEqual(fakes.DEFAULT_CONFIG, self.base.config) + self.assertEqual(parser.get_default('validation_dir'), 'foo') + + @mock.patch('os.path.abspath', return_value='/foo') + @mock.patch('validations_libs.utils.load_config', + return_value=fakes.DEFAULT_CONFIG) + def test_argument_parser_config_choice(self, mock_load, mock_path): + arglist = [] + verifylist = [] + self._set_args(arglist) + cmd_parser = self.cmd.get_parser('check_parser') + parser = self.base.set_argument_parser(cmd_parser) + + self.assertEqual(fakes.DEFAULT_CONFIG, self.base.config) + self.assertEqual(parser.get_default('validation_dir'), + '/usr/share/ansible/validation-playbooks') + + @mock.patch('os.path.abspath', return_value='/foo') + @mock.patch('validations_libs.utils.load_config', + return_value={}) + def test_argument_parser_constant_choice(self, mock_load, mock_path): + arglist = [] + verifylist = [] + self._set_args(arglist) + cmd_parser = self.cmd.get_parser('check_parser') + parser = self.base.set_argument_parser(cmd_parser) + + self.assertEqual({}, self.base.config) + self.assertEqual(parser.get_default('validation_dir'), + '/usr/share/ansible/validation-playbooks') diff --git a/validations_libs/tests/cli/test_history.py b/validations_libs/tests/cli/test_history.py index c5b98639..77e9794b 100644 --- a/validations_libs/tests/cli/test_history.py +++ b/validations_libs/tests/cli/test_history.py @@ -34,6 +34,7 @@ class TestListHistory(BaseCommand): arglist = ['--validation-log-dir', '/foo/log/dir'] verifylist = [('validation_log_dir', '/foo/log/dir')] + self._set_args(arglist) col = ('UUID', 'Validations', 'Status', 'Execution at', 'Duration') values = [('008886df-d297-1eaa-2a74-000000000008', '512e', 'PASSED', @@ -44,6 +45,33 @@ class TestListHistory(BaseCommand): result = self.cmd.take_action(parsed_args) self.assertEqual(result, (col, values)) + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_history') + @mock.patch('validations_libs.utils.load_config', + return_value=fakes.DEFAULT_CONFIG) + def test_list_history_limit_with_config(self, mock_config, mock_history): + arglist = ['--validation-log-dir', '/foo/log/dir'] + verifylist = [('validation_log_dir', '/foo/log/dir')] + self._set_args(arglist) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertEqual(parsed_args.history_limit, 15) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_history') + @mock.patch('validations_libs.utils.load_config', + return_value=fakes.WRONG_HISTORY_CONFIG) + def test_list_history_limit_with_wrong_config(self, mock_config, + mock_history): + arglist = ['--validation-log-dir', '/foo/log/dir'] + verifylist = [('validation_log_dir', '/foo/log/dir')] + self._set_args(arglist) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertEqual(parsed_args.history_limit, 0) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(ValueError, self.cmd.take_action, parsed_args) + class TestGetHistory(BaseCommand): @@ -57,6 +85,7 @@ class TestGetHistory(BaseCommand): def test_get_history(self, mock_logs): arglist = ['123'] verifylist = [('uuid', '123')] + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) @@ -68,6 +97,7 @@ class TestGetHistory(BaseCommand): arglist = ['123', '--validation-log-dir', '/foo/log/dir'] verifylist = [('uuid', '123'), ('validation_log_dir', '/foo/log/dir')] + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) @@ -78,5 +108,6 @@ class TestGetHistory(BaseCommand): arglist = ['123', '--full'] verifylist = [('uuid', '123'), ('full', True)] + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) diff --git a/validations_libs/tests/cli/test_run.py b/validations_libs/tests/cli/test_run.py index afce1b8a..cb1159e7 100644 --- a/validations_libs/tests/cli/test_run.py +++ b/validations_libs/tests/cli/test_run.py @@ -34,24 +34,25 @@ class TestRun(BaseCommand): 'run_validations', return_value=None) def test_run_command_return_none(self, mock_run): - arglist = ['--validation', 'foo'] + args = self._set_args(['--validation', 'foo']) verifylist = [('validation_name', ['foo'])] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, args, verifylist) self.assertRaises(RuntimeError, self.cmd.take_action, parsed_args) @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) def test_run_command_success(self, mock_run): - arglist = ['--validation', 'foo'] + args = self._set_args(['--validation', 'foo']) verifylist = [('validation_name', ['foo'])] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, args, verifylist) self.cmd.take_action(parsed_args) def test_run_command_exclusive_group(self): arglist = ['--validation', 'foo', '--group', 'bar'] + self._set_args(arglist) verifylist = [('validation_name', ['foo'], 'group', 'bar')] self.assertRaises(Exception, self.check_parser, self.cmd, @@ -64,8 +65,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) - def test_run_command_extra_vars(self, mock_run, mock_user, mock_print, - mock_log_dir): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_extra_vars(self, mock_config, mock_run, + mock_user, mock_print, mock_log_dir): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -80,13 +82,15 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-vars', 'key=value'] verifylist = [('validation_name', ['foo']), ('extra_vars', {'key': 'value'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -98,8 +102,10 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) - def test_run_command_extra_vars_twice(self, mock_run, mock_user, - mock_print, mock_log_dir): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_extra_vars_twice(self, mock_config, + mock_run, mock_user, mock_print, + mock_log_dir): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -114,14 +120,16 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-vars', 'key=value1', '--extra-vars', 'key=value2'] verifylist = [('validation_name', ['foo']), ('extra_vars', {'key': 'value2'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -144,7 +152,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) - def test_run_command_extra_vars_file(self, mock_run, mock_user, mock_open, + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_extra_vars_file(self, mock_config, mock_run, + mock_user, mock_open, mock_yaml, mock_log_dir): run_called_args = { @@ -161,13 +171,15 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-vars-file', '/foo/vars.yaml'] verifylist = [('validation_name', ['foo']), ('extra_vars_file', '/foo/vars.yaml')] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -178,7 +190,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) - def test_run_command_extra_env_vars(self, mock_run, mock_user, mock_log_dir): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_extra_env_vars(self, mock_config, mock_run, + mock_user, mock_log_dir): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -193,13 +207,15 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-env-vars', 'key=value'] verifylist = [('validation_name', ['foo']), ('extra_env_vars', {'key': 'value'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -210,7 +226,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) + @mock.patch('validations_libs.utils.load_config', return_value={}) def test_run_command_extra_env_vars_with_custom_callback(self, + mock_config, mock_run, mock_user, mock_log_dir): @@ -229,13 +247,15 @@ class TestRun(BaseCommand): 'extra_env_vars': {'ANSIBLE_STDOUT_CALLBACK': 'default'}, 'python_interpreter': sys.executable, 'quiet': False, - 'ssh_user': 'doe'} + 'ssh_user': 'doe', + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-env-vars', 'ANSIBLE_STDOUT_CALLBACK=default'] verifylist = [('validation_name', ['foo']), ('extra_env_vars', {'ANSIBLE_STDOUT_CALLBACK': 'default'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -246,7 +266,10 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) - def test_run_command_extra_env_vars_twice(self, mock_run, mock_user, mock_log_dir): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_extra_env_vars_twice(self, mock_config, + mock_run, mock_user, + mock_log_dir): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -261,14 +284,16 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-env-vars', 'key=value1', '--extra-env-vars', 'key=value2'] verifylist = [('validation_name', ['foo']), ('extra_env_vars', {'key': 'value2'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -279,7 +304,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_SUCCESS_RUN)) + @mock.patch('validations_libs.utils.load_config', return_value={}) def test_run_command_extra_env_vars_and_extra_vars(self, + mock_config, mock_run, mock_user, mock_log_dir): @@ -297,7 +324,9 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo', '--extra-vars', 'key=value', @@ -305,7 +334,7 @@ class TestRun(BaseCommand): verifylist = [('validation_name', ['foo']), ('extra_vars', {'key': 'value'}), ('extra_env_vars', {'key2': 'value2'})] - + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) mock_run.assert_called_with(**run_called_args) @@ -325,7 +354,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=copy.deepcopy(fakes.FAKE_FAILED_RUN)) - def test_run_command_failed_validation(self, mock_run, mock_user, mock_log_dir): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_failed_validation(self, mock_config, + mock_run, mock_user, mock_log_dir): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -340,10 +371,14 @@ class TestRun(BaseCommand): 'python_interpreter': sys.executable, 'quiet': True, 'ssh_user': 'doe', - 'log_path': mock_log_dir} + 'log_path': mock_log_dir, + 'validation_config': {} + } arglist = ['--validation', 'foo'] verifylist = [('validation_name', ['foo'])] + + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(RuntimeError, self.cmd.take_action, parsed_args) mock_run.assert_called_with(**run_called_args) @@ -353,7 +388,9 @@ class TestRun(BaseCommand): @mock.patch('validations_libs.validation_actions.ValidationActions.' 'run_validations', return_value=[]) - def test_run_command_no_validation(self, mock_run, mock_user): + @mock.patch('validations_libs.utils.load_config', return_value={}) + def test_run_command_no_validation(self, mock_config, mock_run, + mock_user): run_called_args = { 'inventory': 'localhost', 'limit_hosts': None, @@ -367,10 +404,85 @@ class TestRun(BaseCommand): 'extra_env_vars': {'key2': 'value2'}, 'python_interpreter': sys.executable, 'quiet': True, - 'ssh_user': 'doe'} + 'ssh_user': 'doe', + 'validation_config': {} + } arglist = ['--validation', 'foo'] verifylist = [('validation_name', ['foo'])] + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(RuntimeError, self.cmd.take_action, parsed_args) + + @mock.patch('validations_libs.constants.VALIDATIONS_LOG_BASEDIR') + @mock.patch('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + def test_run_with_wrong_config(self, mock_run, + mock_user, mock_log_dir): + arglist = ['--validation', 'foo', '--config', 'wrong.cfg'] + verifylist = [('validation_name', ['foo']), + ('config', 'wrong.cfg')] + + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'category': [], + 'product': [], + 'extra_vars': None, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible', + 'validation_name': ['foo'], + 'extra_env_vars': None, + 'python_interpreter': sys.executable, + 'quiet': True, + 'ssh_user': 'doe', + 'log_path': mock_log_dir, + 'validation_config': {} + } + + self._set_args(arglist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + mock_run.assert_called_with(**run_called_args) + + @mock.patch('validations_libs.constants.VALIDATIONS_LOG_BASEDIR') + @mock.patch('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + @mock.patch('os.path.exists', return_value=True) + def test_run_with_config(self, mock_exists, + mock_run, mock_user, + mock_log_dir): + arglist = ['--validation', 'foo', '--config', 'config.cfg'] + verifylist = [('validation_name', ['foo']), + ('config', 'config.cfg')] + + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'category': [], + 'product': [], + 'extra_vars': None, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible', + 'validation_name': ['foo'], + 'extra_env_vars': None, + 'python_interpreter': sys.executable, + 'quiet': True, + 'ssh_user': 'doe', + 'log_path': mock_log_dir, + 'validation_config': {} + } + + self._set_args(arglist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + mock_run.assert_called_with(**run_called_args) diff --git a/validations_libs/tests/cli/test_show.py b/validations_libs/tests/cli/test_show.py index 31d7ae64..c4deb82f 100644 --- a/validations_libs/tests/cli/test_show.py +++ b/validations_libs/tests/cli/test_show.py @@ -33,6 +33,7 @@ class TestShow(BaseCommand): def test_show_validations(self, mock_show): arglist = ['foo'] verifylist = [('validation_name', 'foo')] + self._set_args(arglist) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) diff --git a/validations_libs/tests/fakes.py b/validations_libs/tests/fakes.py index 598c946c..d80455e2 100644 --- a/validations_libs/tests/fakes.py +++ b/validations_libs/tests/fakes.py @@ -318,6 +318,23 @@ FAKE_FAILED_RUN = [{'Duration': '0:00:01.761', FAKE_VALIDATIONS_PATH = '/usr/share/ansible/validation-playbooks' +DEFAULT_CONFIG = {'validation_dir': '/usr/share/ansible/validation-playbooks', + 'ansible_base_dir': '/usr/share/ansible/', + 'output_log': 'output.log', + 'history_limit': 15, + 'fit_width': True} + +WRONG_HISTORY_CONFIG = {'default': {'history_limit': 0}} + +ANSIBLE_RUNNER_CONFIG = {'verbosity': 5, + 'fact_cache_type': 'jsonfile', + 'quiet': True, 'rotate_artifacts': 256} + +ANSIBLE_ENVIRONNMENT_CONFIG = {'ANSIBLE_CALLBACK_WHITELIST': + 'validation_stdout,validation_json,' + 'profile_tasks', + 'ANSIBLE_STDOUT_CALLBACK': 'validation_stdout'} + def fake_ansible_runner_run_return(status='successful', rc=0): return status, rc diff --git a/validations_libs/tests/test_ansible.py b/validations_libs/tests/test_ansible.py index edc09c1b..827462ce 100644 --- a/validations_libs/tests/test_ansible.py +++ b/validations_libs/tests/test_ansible.py @@ -348,3 +348,83 @@ class TestAnsible(TestCase): }) mock_config.assert_called_once_with(**opt) + + @mock.patch('os.path.exists', return_value=True) + @mock.patch('os.makedirs') + @mock.patch.object(Runner, 'run', + return_value=fakes.fake_ansible_runner_run_return(rc=0)) + @mock.patch('ansible_runner.utils.dump_artifact', autospec=True, + return_value="/foo/inventory.yaml") + @mock.patch('six.moves.builtins.open') + @mock.patch('ansible_runner.runner_config.RunnerConfig') + def test_run_success_with_config(self, mock_config, mock_open, + mock_dump_artifact, mock_run, + mock_mkdirs, mock_exists + ): + fake_config = {'default': fakes.DEFAULT_CONFIG, + 'ansible_environment': + fakes.ANSIBLE_ENVIRONNMENT_CONFIG, + 'ansible_runner': fakes.ANSIBLE_RUNNER_CONFIG + } + _playbook, _rc, _status = self.run.run( + playbook='existing.yaml', + inventory='localhost,', + workdir='/tmp', + connection='local', + ansible_artifact_path='/tmp', + validation_cfg_file=fake_config + ) + self.assertEqual((_playbook, _rc, _status), + ('existing.yaml', 0, 'successful')) + mock_open.assert_called_with('/tmp/validation.cfg', 'w') + + @mock.patch('os.path.exists', return_value=True) + @mock.patch('os.makedirs') + @mock.patch.object(Runner, 'run', + return_value=fakes.fake_ansible_runner_run_return(rc=0)) + @mock.patch('ansible_runner.utils.dump_artifact', autospec=True, + return_value="/foo/inventory.yaml") + @mock.patch('six.moves.builtins.open') + @mock.patch('ansible_runner.runner_config.RunnerConfig') + def test_run_success_with_empty_config(self, mock_config, mock_open, + mock_dump_artifact, mock_run, + mock_mkdirs, mock_exists + ): + fake_config = {} + _playbook, _rc, _status = self.run.run( + playbook='existing.yaml', + inventory='localhost,', + workdir='/tmp', + connection='local', + ansible_cfg_file='/foo.cfg', + ansible_artifact_path='/tmp', + validation_cfg_file=fake_config + ) + self.assertEqual((_playbook, _rc, _status), + ('existing.yaml', 0, 'successful')) + mock_open.assert_not_called() + + @mock.patch('os.path.exists', return_value=True) + @mock.patch('os.makedirs') + @mock.patch.object(Runner, 'run', + return_value=fakes.fake_ansible_runner_run_return(rc=0)) + @mock.patch('ansible_runner.utils.dump_artifact', autospec=True, + return_value="/foo/inventory.yaml") + @mock.patch('six.moves.builtins.open') + @mock.patch('ansible_runner.runner_config.RunnerConfig') + def test_run_success_with_ansible_config(self, mock_config, mock_open, + mock_dump_artifact, mock_run, + mock_mkdirs, mock_exists + ): + fake_config = {} + _playbook, _rc, _status = self.run.run( + playbook='existing.yaml', + inventory='localhost,', + workdir='/tmp', + connection='local', + ansible_artifact_path='/tmp', + validation_cfg_file=fake_config + ) + self.assertEqual((_playbook, _rc, _status), + ('existing.yaml', 0, 'successful')) + mock_open.assert_called_with('/tmp/ansible.cfg', 'w') diff --git a/validations_libs/tests/test_utils.py b/validations_libs/tests/test_utils.py index c68f353e..0906f738 100644 --- a/validations_libs/tests/test_utils.py +++ b/validations_libs/tests/test_utils.py @@ -389,3 +389,40 @@ class TestUtils(TestCase): """Test if failure to create artifacts dir raises 'RuntimeError'. """ self.assertRaises(RuntimeError, utils.create_artifacts_dir, "/foo/bar") + + def test_eval_types_str(self): + self.assertIsInstance(utils._eval_types('/usr'), str) + + def test_eval_types_bool(self): + self.assertIsInstance(utils._eval_types('True'), bool) + + def test_eval_types_int(self): + self.assertIsInstance(utils._eval_types('15'), int) + + def test_eval_types_dict(self): + self.assertIsInstance(utils._eval_types('{}'), dict) + + @mock.patch('os.path.exists', return_value=True) + @mock.patch('configparser.ConfigParser.sections', + return_value=['default']) + def test_load_config(self, mock_config, mock_exists): + results = utils.load_config('foo.cfg') + self.assertEqual(results, {}) + + def test_default_load_config(self): + results = utils.load_config('validation.cfg') + self.assertEqual(results['default'], fakes.DEFAULT_CONFIG) + + def test_ansible_runner_load_config(self): + results = utils.load_config('validation.cfg') + self.assertEqual(results['ansible_runner'], + fakes.ANSIBLE_RUNNER_CONFIG) + + def test_ansible_environment_config_load_config(self): + results = utils.load_config('validation.cfg') + self.assertEqual( + results['ansible_environment']['ANSIBLE_CALLBACK_WHITELIST'], + fakes.ANSIBLE_ENVIRONNMENT_CONFIG['ANSIBLE_CALLBACK_WHITELIST']) + self.assertEqual( + results['ansible_environment']['ANSIBLE_STDOUT_CALLBACK'], + fakes.ANSIBLE_ENVIRONNMENT_CONFIG['ANSIBLE_STDOUT_CALLBACK']) diff --git a/validations_libs/tests/test_validation_actions.py b/validations_libs/tests/test_validation_actions.py index e8bfaa52..55bfeff8 100644 --- a/validations_libs/tests/test_validation_actions.py +++ b/validations_libs/tests/test_validation_actions.py @@ -104,13 +104,14 @@ class TestValidationActions(TestCase): 'extra_vars': None, 'limit_hosts': '!cloud1', 'extra_env_variables': None, - 'ansible_cfg': None, + 'ansible_cfg_file': None, 'gathering_policy': 'explicit', 'ansible_artifact_path': '/var/log/validations/artifacts/123_fake.yaml_time', 'log_path': '/var/log/validations', 'run_async': False, 'python_interpreter': None, - 'ssh_user': None + 'ssh_user': None, + 'validation_cfg_file': None } playbook = ['fake.yaml'] @@ -164,13 +165,14 @@ class TestValidationActions(TestCase): 'extra_vars': None, 'limit_hosts': '!cloud1,cloud,!cloud2', 'extra_env_variables': None, - 'ansible_cfg': None, + 'ansible_cfg_file': None, 'gathering_policy': 'explicit', 'ansible_artifact_path': '/var/log/validations/artifacts/123_fake.yaml_time', 'log_path': '/var/log/validations', 'run_async': False, 'python_interpreter': None, - 'ssh_user': None + 'ssh_user': None, + 'validation_cfg_file': None } playbook = ['fake.yaml'] diff --git a/validations_libs/utils.py b/validations_libs/utils.py index ae9a1caf..29433514 100644 --- a/validations_libs/utils.py +++ b/validations_libs/utils.py @@ -12,11 +12,15 @@ # License for the specific language governing permissions and limitations # under the License. # +import ast +import configparser import datetime import glob import logging import os +import site import six +import sys import uuid from os.path import join @@ -488,3 +492,94 @@ def get_validations_parameters(validations_data, } return params + + +def _eval_types(value): + try: + return int(value) + except ValueError: + pass + try: + return ast.literal_eval(value) + except (SyntaxError, NameError, ValueError): + pass + try: + return str(value) + except ValueError: + msg = ("Can not eval or type not supported for value: {},").format( + value) + raise ValueError(msg) + + +def load_config(config): + """Load Config File from CLI""" + if not os.path.exists(config): + msg = ("Config file {} could not be found, ignoring...").format(config) + LOG.warning(msg) + return {} + else: + msg = "Validation config file found: {}".format(config) + LOG.info(msg) + parser = configparser.ConfigParser() + parser.optionxform = str + parser.read(config) + data = {} + try: + for section in parser.sections(): + for keys, values in parser.items(section): + if section not in data: + # Init section in dictionary + data[section] = {} + if section == 'ansible_environment': + # for Ansible environment variables we dont want to cast + # types, each values should a type String. + data[section][keys] = values + elif section == 'ansible_runner' and \ + keys not in constants.ANSIBLE_RUNNER_CONFIG_PARAMETERS: + # for Ansible runner parameters, we select only a set + # of parameters which will be passed as **kwargs in the + # runner, so we have to ignore all the others. + msg = ("Incompatible key found for ansible_runner section {}, " + "ignoring {} ...").format(section, keys) + LOG.warning(msg) + continue + else: + data[section][keys] = _eval_types(values) + except configparser.NoSectionError: + msg = ("Wrong format for the config file {}, " + "section {} can not be found, ignoring...").format(config, + section) + LOG.warning(msg) + return {} + return data + + +def find_config_file(config_file_name='validation.cfg'): + """ Find the config file for Validation in the following order: + * environment validation VALIDATION_CONFIG + * current user directory + * user home directory + * Python prefix path which has been used for the installation + * /etc/validation.cfg + """ + def _check_path(path): + if os.path.exists(path): + if os.path.isfile(path) and os.access(path, + os.R_OK): + return path + # Build a list of potential paths with the correct order: + paths = [] + env_config = os.getenv("VALIDATION_CONFIG", "") + if _check_path(env_config): + return env_config + paths.append(os.getcwd()) + paths.append(os.path.expanduser('~')) + for prefix in site.PREFIXES: + paths.append(os.path.join(prefix, 'etc')) + paths.append('/etc') + + for path in paths: + current_path = os.path.join(path, config_file_name) + if _check_path(current_path): + return current_path + return current_path diff --git a/validations_libs/validation_actions.py b/validations_libs/validation_actions.py index 2da9a64e..1eddb43e 100644 --- a/validations_libs/validation_actions.py +++ b/validations_libs/validation_actions.py @@ -267,7 +267,8 @@ class ValidationActions(object): log_path=constants.VALIDATIONS_LOG_BASEDIR, python_interpreter=None, skip_list=None, callback_whitelist=None, - output_callback='validation_stdout', ssh_user=None): + output_callback='validation_stdout', ssh_user=None, + validation_config=None): """Run one or multiple validations by name(s), by group(s) or by product(s) @@ -336,6 +337,9 @@ class ValidationActions(object): :rtype: ``list`` :param ssh_user: Ssh user for Ansible remote connection :type ssh_user: ``string`` + :param validation_config: A dictionary of configuration for Validation + loaded from an validation.cfg file. + :type validation_config: ``dict`` :Example: @@ -424,13 +428,14 @@ class ValidationActions(object): extra_vars=extra_vars, limit_hosts=_hosts, extra_env_variables=extra_env_vars, - ansible_cfg=ansible_cfg, + ansible_cfg_file=ansible_cfg, gathering_policy='explicit', ansible_artifact_path=artifacts_dir, log_path=log_path, run_async=run_async, python_interpreter=python_interpreter, - ssh_user=ssh_user) + ssh_user=ssh_user, + validation_cfg_file=validation_config) else: _playbook, _rc, _status = run_ansible.run( workdir=artifacts_dir, @@ -445,13 +450,14 @@ class ValidationActions(object): extra_vars=extra_vars, limit_hosts=_hosts, extra_env_variables=extra_env_vars, - ansible_cfg=ansible_cfg, + ansible_cfg_file=ansible_cfg, gathering_policy='explicit', ansible_artifact_path=artifacts_dir, log_path=log_path, run_async=run_async, python_interpreter=python_interpreter, - ssh_user=ssh_user) + ssh_user=ssh_user, + validation_cfg_file=validation_config) results.append({'playbook': _playbook, 'rc_code': _rc, 'status': _status,