diff --git a/requirements.txt b/requirements.txt index 5e51a97b..c2abb7a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ six>=1.11.0 # MIT PyYAML>=3.13 # MIT ansible>=2.8,!=2.8.9,!=2.9.12,<2.10.0 ansible-runner>=1.4.0 # Apache-2.0 +cliff>=3.2.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 3698b1bb..10b33051 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,3 +36,14 @@ mapping_file = babel.cfg output_file = validations-libs/locale/validations-libs.pot [entry_points] +console_scripts: + validation = validations_libs.cli.app:main + +validation.cli: + list = validations_libs.cli.lister:ValidationList + show = validations_libs.cli.show:Show + show_group = validations_libs.cli.show:ShowGroup + show_parameter = validations_libs.cli.show:ShowParameter + run = validations_libs.cli.run:Run + history_list = validations_libs.cli.history:ListHistory + history_get = validations_libs.cli.history:GetHistory diff --git a/validations_libs/cli/__init__.py b/validations_libs/cli/__init__.py new file mode 100644 index 00000000..ff3eb400 --- /dev/null +++ b/validations_libs/cli/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +# 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. diff --git a/validations_libs/cli/app.py b/validations_libs/cli/app.py new file mode 100644 index 00000000..0574593e --- /dev/null +++ b/validations_libs/cli/app.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python + +# 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 + +from cliff.app import App +from cliff.commandmanager import CommandManager + + +class ValidationCliApp(App): + """Cliff application for the `ValidationCli` tool. + :param description: one-liner explaining the program purpose + :param version: application version number + :param command_manager: plugin loader + :param deferred_help: Allow subcommands to accept `–help` with allowing + to defer help print after initialize_app + """ + + def __init__(self): + super(ValidationCliApp, self).__init__( + description="Validations Framework Command Line Interface (CLI)", + version='1.0', + command_manager=CommandManager('validation.cli'), + deferred_help=True, + ) + + def initialize_app(self, argv): + self.LOG.debug('Initialize Validation App.') + + def prepare_to_run_command(self, cmd): + self.LOG.debug('prepare_to_run_command %s', cmd.__class__.__name__) + + def clean_up(self, cmd, result, err): + self.LOG.debug('clean_up %s', cmd.__class__.__name__) + if err: + self.LOG.debug('got an error: %s', err) + + +def main(argv=sys.argv[1:]): + v_cli = ValidationCliApp() + return v_cli.run(argv) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/validations_libs/cli/common.py b/validations_libs/cli/common.py new file mode 100644 index 00000000..703d034a --- /dev/null +++ b/validations_libs/cli/common.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python + +# 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 json +from prettytable import PrettyTable + +from validations_libs import constants +from validations_libs import utils as v_utils + +GROUP_FILE = constants.VALIDATION_GROUPS_INFO + +# PrettyTable Colors: +RED = "\033[1;31m" +GREEN = "\033[0;32m" +CYAN = "\033[36m" +RESET = "\033[0;0m" +YELLOW = "\033[0;33m" + + +def print_dict(data): + """Print table from python dict with PrettyTable""" + table = PrettyTable(border=True, header=True, padding_width=1) + # Set Field name by getting the result dict keys + try: + table.field_names = data[0].keys() + table.align = 'l' + except IndexError: + raise IndexError() + for row in data: + if row.get('Status_by_Host'): + hosts = [] + for host in row['Status_by_Host'].split(', '): + try: + _name, _status = host.split(',') + except ValueError: + # if ValueError, then host is in unknown state: + _name = host + _status = 'UNKNOWN' + color = (GREEN if _status == 'PASSED' else + (YELLOW if _status == 'UNREACHABLE' else RED)) + _name = '{}{}{}'.format(color, _name, RESET) + hosts.append(_name) + row['Status_by_Host'] = ', '.join(hosts) + if row.get('Status'): + status = row.get('Status') + color = (CYAN if status in ['starting', 'running'] + else GREEN if status == 'PASSED' else RED) + row['Status'] = '{}{}{}'.format(color, status, RESET) + table.add_row(row.values()) + print(table) + + +def write_output(output_log, results): + """Write output log file as Json format""" + with open(output_log, 'w') as output: + output.write(json.dumps({'results': results}, indent=4, + sort_keys=True)) diff --git a/validations_libs/cli/history.py b/validations_libs/cli/history.py new file mode 100644 index 00000000..59dcc4d7 --- /dev/null +++ b/validations_libs/cli/history.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +# 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 json +import os +import sys + +from cliff.command import Command +from cliff.lister import Lister + +from validations_libs import constants +from validations_libs.validation_actions import ValidationActions +from validations_libs.validation_logs import ValidationLogs + + +class ListHistory(Lister): + """Display Validations execution history""" + + def get_parser(self, parser): + parser = super(ListHistory, self).get_parser(parser) + + parser.add_argument('--validation', + metavar="", + type=str, + help='Display execution history for a validation') + parser.add_argument('--validation-log-dir', dest='validation_log_dir', + default=constants.VALIDATIONS_LOG_BASEDIR, + help=("Path where the validation log files " + "is located.")) + return parser + + def take_action(self, parsed_args): + actions = ValidationActions(parsed_args.validation_log_dir) + return actions.show_history(parsed_args.validation) + + +class GetHistory(Command): + """Display details about a Validation execution""" + + def get_parser(self, parser): + parser = super(GetHistory, self).get_parser(parser) + parser.add_argument('uuid', + metavar="", + type=str, + help='Validation UUID Run') + + parser.add_argument('--full', + action='store_true', + help='Show Full Details for the run') + + parser.add_argument('--validation-log-dir', dest='validation_log_dir', + default=constants.VALIDATIONS_LOG_BASEDIR, + help=("Path where the validation log files " + "is located.")) + return parser + + def take_action(self, parsed_args): + vlogs = ValidationLogs(logs_path=parsed_args.validation_log_dir) + data = vlogs.get_logfile_content_by_uuid(parsed_args.uuid) + if data: + if parsed_args.full: + for d in data: + print(json.dumps(d, indent=4, sort_keys=True)) + else: + for d in data: + for p in d.get('validation_output', []): + print(json.dumps(p['task'], + indent=4, + sort_keys=True)) + else: + raise RuntimeError( + "Could not find the log file linked to this UUID: %s" % + parsed_args.uuid) diff --git a/validations_libs/cli/lister.py b/validations_libs/cli/lister.py new file mode 100644 index 00000000..32bd4194 --- /dev/null +++ b/validations_libs/cli/lister.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# 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 json +import sys + +from cliff.lister import Lister + +from validations_libs.validation_actions import ValidationActions +from validations_libs import constants +from validations_libs.cli.parseractions import CommaListAction + + +class ValidationList(Lister): + """Validation List client implementation class""" + + def get_parser(self, parser): + """Argument parser for validation run""" + parser = super(ValidationList, self).get_parser(parser) + parser.add_argument('--group', '-g', + metavar='[,,...]', + action=CommaListAction, + default=[], + help=("Run specific group validations, " + "if more than one group is required " + "separate the group names with commas: " + "--group pre-upgrade,prep | " + "--group openshift-on-openstack")) + parser.add_argument('--validation-dir', dest='validation_dir', + default=constants.ANSIBLE_VALIDATION_DIR, + help=("Path where the validation playbooks " + "is located.")) + return parser + + def take_action(self, parsed_args): + """Take validation action""" + # Get parameters: + group = parsed_args.group + validation_dir = parsed_args.validation_dir + + v_actions = ValidationActions(validation_path=validation_dir) + return (v_actions.list_validations(group)) diff --git a/validations_libs/cli/parseractions.py b/validations_libs/cli/parseractions.py new file mode 100644 index 00000000..68b4e116 --- /dev/null +++ b/validations_libs/cli/parseractions.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +# 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 argparse + + +class CommaListAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values.split(',')) + + +class KeyValueAction(argparse.Action): + """A custom action to parse arguments as key=value pairs + Ensures that ``dest`` is a dict and values are strings. + """ + + def __call__(self, parser, namespace, values, option_string=None): + # Make sure we have an empty dict rather than None + if getattr(namespace, self.dest, None) is None: + setattr(namespace, self.dest, {}) + + # Add value if an assignment else remove it + if '=' in values and values.count('=') == 1: + values_list = values.split('=', 1) + if '' == values_list[0]: + msg = ("Property key must be specified: %s") + raise argparse.ArgumentTypeError(msg % str(values)) + else: + getattr(namespace, self.dest, {}).update([values_list]) + else: + msg = ("Expected 'key=value' type, but got: %s") + raise argparse.ArgumentTypeError(msg % str(values)) diff --git a/validations_libs/cli/run.py b/validations_libs/cli/run.py new file mode 100644 index 00000000..bb3f3756 --- /dev/null +++ b/validations_libs/cli/run.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python + +# 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 getpass +import json +import os +import sys +import yaml + +from cliff.command import Command + +from validations_libs import constants +from validations_libs.validation_actions import ValidationActions +from validations_libs.cli import common +from validations_libs.cli.parseractions import CommaListAction, KeyValueAction + + +class Run(Command): + """Validation Run client implementation class""" + + def get_parser(self, parser): + """Argument parser for validation run""" + parser = super(Run, self).get_parser(parser) + parser.add_argument( + '--limit', action='store', required=False, help=( + "A string that identifies a single node or comma-separated " + "list of nodes to be upgraded in parallel in this upgrade " + " run invocation. For example: --limit \"compute-0," + " compute-1, compute-5\".") + ) + + parser.add_argument( + '--ssh-user', + dest='ssh_user', + default=getpass.getuser(), + help=("Ssh User name for the Ansible ssh connection.") + ) + parser.add_argument('--validation-dir', dest='validation_dir', + default=constants.ANSIBLE_VALIDATION_DIR, + help=("Path where the validation playbooks " + "is located.")) + + parser.add_argument('--ansible-base-dir', dest='ansible_base_dir', + default=constants.DEFAULT_VALIDATIONS_BASEDIR, + help=("Path where the ansible roles, library " + "and plugins are located.")) + + parser.add_argument('--inventory', '-i', type=str, + default="localhost", + help="Path of the Ansible inventory.") + + parser.add_argument('--output-log', dest='output_log', + default=None, + help=("Path where the run result will be stored.")) + + parser.add_argument( + '--extra-env-vars', + action=KeyValueAction, + default=None, + metavar="key1= [--extra-vars key3=]", + help=( + " Add extra environment variables you may need " + "to provide to your Ansible execution " + "as KEY=VALUE pairs. Note that if you pass the same " + "KEY multiple times, the last given VALUE for that same KEY " + "will override the other(s)") + ) + + extra_vars_group = parser.add_mutually_exclusive_group(required=False) + extra_vars_group.add_argument( + '--extra-vars', + default=None, + metavar="key1= [--extra-vars key3=]", + action=KeyValueAction, + help=( + "Add Ansible extra variables to the validation(s) execution " + "as KEY=VALUE pair(s). Note that if you pass the same " + "KEY multiple times, the last given VALUE for that same KEY " + "will override the other(s)") + ) + + extra_vars_group.add_argument( + '--extra-vars-file', + action='store', + default=None, + help=( + "Add a JSON/YAML file containing extra variable " + "to a validation: " + "--extra-vars-file /home/stack/vars.[json|yaml]." + ) + ) + + ex_group = parser.add_mutually_exclusive_group(required=True) + ex_group.add_argument( + '--validation', + metavar='[,,...]', + dest="validation_name", + action=CommaListAction, + default=[], + help=("Run specific validations, " + "if more than one validation is required " + "separate the names with commas: " + "--validation check-ftype,512e | " + "--validation 512e") + ) + + ex_group.add_argument( + '--group', '-g', + metavar='[,,...]', + action=CommaListAction, + default=[], + help=("Run specific group validations, " + "if more than one group is required " + "separate the group names with commas: " + "--group pre-upgrade,prep | " + "--group openshift-on-openstack") + ) + + return parser + + def take_action(self, parsed_args): + """Take validation action""" + v_actions = ValidationActions( + validation_path=parsed_args.validation_dir) + + extra_vars = parsed_args.extra_vars + if parsed_args.extra_vars_file: + try: + with open(parsed_args.extra_vars_file, 'r') as env_file: + extra_vars = yaml.safe_load(env_file.read()) + except yaml.YAMLError as e: + error_msg = ( + "The extra_vars file must be properly formatted YAML/JSON." + "Details: %s." % e) + raise RuntimeError(error_msg) + + try: + results = v_actions.run_validations( + inventory=parsed_args.inventory, + limit_hosts=parsed_args.limit, + group=parsed_args.group, + extra_vars=extra_vars, + validations_dir=parsed_args.validation_dir, + base_dir=parsed_args.ansible_base_dir, + validation_name=parsed_args.validation_name, + extra_env_vars=parsed_args.extra_env_vars, + quiet=True, + ssh_user=parsed_args.ssh_user) + except RuntimeError as e: + raise RuntimeError(e) + + _rc = None + if results: + _rc = any([1 for r in results if r['Status'] == 'FAILED']) + + if parsed_args.output_log: + common.write_output(parsed_args.output_log, results) + common.print_dict(results) + + if _rc: + raise RuntimeError("One or more validations have failed.") diff --git a/validations_libs/cli/show.py b/validations_libs/cli/show.py new file mode 100644 index 00000000..ddc2309d --- /dev/null +++ b/validations_libs/cli/show.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python + +# 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 json +import sys + +from cliff.show import ShowOne + +from validations_libs.validation_actions import ValidationActions +from validations_libs import constants +from validations_libs.cli.parseractions import CommaListAction + + +class Show(ShowOne): + """Validation Show client implementation class""" + + def get_parser(self, parser): + """Argument parser for validation show""" + parser = super(Show, self).get_parser(parser) + parser.add_argument('--validation-dir', dest='validation_dir', + default=constants.ANSIBLE_VALIDATION_DIR, + help=("Path where the validation playbooks " + "is located.")) + parser.add_argument('validation_name', + metavar="", + type=str, + help="Show a specific validation.") + return parser + + def take_action(self, parsed_args): + """Take validation action""" + # Get parameters: + validation_dir = parsed_args.validation_dir + validation_name = parsed_args.validation_name + + v_actions = ValidationActions(validation_path=validation_dir) + data = v_actions.show_validations(validation_name) + + if data: + return data.keys(), data.values() + + +class ShowGroup(ShowOne): + """Validation Show group client implementation class""" + + def get_parser(self, parser): + """Argument parser for validation show group""" + parser = super(ShowGroup, self).get_parser(parser) + parser.add_argument('--validation-dir', dest='validation_dir', + default=constants.ANSIBLE_VALIDATION_DIR, + help=("Path where the validation playbooks " + "is located.")) + parser.add_argument('--group', '-g', + metavar='', + dest="group", + help=("Show a specific group.")) + return parser + + def take_action(self, parsed_args): + """Take validation action""" + # Get parameters: + validation_dir = parsed_args.validation_dir + group = parsed_args.group + + v_actions = ValidationActions(validation_path=validation_dir) + return v_actions.group_information(group) + + +class ShowParameter(ShowOne): + """Display Validations Parameters""" + + def get_parser(self, parser): + parser = super(ShowParameter, self).get_parser(parser) + + parser.add_argument('--validation-dir', dest='validation_dir', + default=constants.ANSIBLE_VALIDATION_DIR, + help=("Path where the validation playbooks " + "is located.")) + + ex_group = parser.add_mutually_exclusive_group(required=False) + ex_group.add_argument( + '--validation', + metavar='[,,...]', + dest='validation_name', + action=CommaListAction, + default=[], + help=("List specific validations, " + "if more than one validation is required " + "separate the names with commas: " + "--validation check-ftype,512e | " + "--validation 512e") + ) + + ex_group.add_argument( + '--group', '-g', + metavar='[,,...]', + action=CommaListAction, + default=[], + help=("List specific group validations, " + "if more than one group is required " + "separate the group names with commas: " + "pre-upgrade,prep | " + "openshift-on-openstack") + ) + + parser.add_argument( + '--download', + action='store', + default=None, + help=("Create a json or a yaml file " + "containing all the variables " + "available for the validations: " + "/tmp/myvars") + ) + + parser.add_argument( + '--format-output', + action='store', + metavar='', + default='json', + choices=['json', 'yaml'], + help=("Print representation of the validation. " + "The choices of the output format is json,yaml. ") + ) + + return parser + + def take_action(self, parsed_args): + v_actions = ValidationActions(parsed_args.validation_dir) + params = v_actions.show_validations_parameters( + parsed_args.validation_name, + parsed_args.group, + parsed_args.format_output, + parsed_args.download) + if parsed_args.download: + print("The file {} has been created successfully".format( + parsed_args.download)) + return params.keys(), params.values() diff --git a/validations_libs/tests/cli/__init__.py b/validations_libs/tests/cli/__init__.py new file mode 100644 index 00000000..dd3055f4 --- /dev/null +++ b/validations_libs/tests/cli/__init__.py @@ -0,0 +1,14 @@ +# 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. +# diff --git a/validations_libs/tests/cli/fakes.py b/validations_libs/tests/cli/fakes.py new file mode 100644 index 00000000..ba801f0e --- /dev/null +++ b/validations_libs/tests/cli/fakes.py @@ -0,0 +1,42 @@ +# 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 app + + +class BaseCommand(TestCase): + + def check_parser(self, cmd, args, verify_args): + cmd_parser = cmd.get_parser('check_parser') + try: + parsed_args = cmd_parser.parse_args(args) + except SystemExit: + raise Exception("Argument parse failed") + for av in verify_args: + attr, value = av + if attr: + self.assertIn(attr, parsed_args) + self.assertEqual(value, getattr(parsed_args, attr)) + return parsed_args + + def setUp(self): + super(BaseCommand, self).setUp() + self.app = app.ValidationCliApp() diff --git a/validations_libs/tests/cli/test_history.py b/validations_libs/tests/cli/test_history.py new file mode 100644 index 00000000..64cd9d8a --- /dev/null +++ b/validations_libs/tests/cli/test_history.py @@ -0,0 +1,83 @@ +# 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 history +from validations_libs.tests import fakes +from validations_libs.tests.cli.fakes import BaseCommand + + +class TestListHistory(BaseCommand): + + def setUp(self): + super(TestListHistory, self).setUp() + self.cmd = history.ListHistory(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_history') + def test_list_history(self, mock_history): + arglist = ['--validation-log-dir', '/foo/log/dir'] + verifylist = [('validation_log_dir', '/foo/log/dir')] + + col = ('UUID', 'Validations', 'Status', 'Execution at', 'Duration') + values = [('008886df-d297-1eaa-2a74-000000000008', + '512e', 'PASSED', + '2019-11-25T13:40:14.404623Z', + '0:00:03.753')] + mock_history.return_value = (col, values) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertEqual(result, (col, values)) + + +class TestGetHistory(BaseCommand): + + def setUp(self): + super(TestGetHistory, self).setUp() + self.cmd = history.GetHistory(self.app, None) + + @mock.patch('validations_libs.validation_logs.ValidationLogs.' + 'get_logfile_content_by_uuid', + return_value=fakes.VALIDATIONS_LOGS_CONTENTS_LIST) + def test_get_history(self, mock_logs): + arglist = ['123'] + verifylist = [('uuid', '123')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + @mock.patch('validations_libs.validation_logs.ValidationLogs.' + 'get_logfile_content_by_uuid', + return_value=fakes.VALIDATIONS_LOGS_CONTENTS_LIST) + def test_get_history_from_log_dir(self, mock_logs): + arglist = ['123', '--validation-log-dir', '/foo/log/dir'] + verifylist = [('uuid', '123'), ('validation_log_dir', '/foo/log/dir')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + @mock.patch('validations_libs.validation_logs.ValidationLogs.' + 'get_logfile_content_by_uuid', + return_value=fakes.VALIDATIONS_LOGS_CONTENTS_LIST) + def test_get_history_full_arg(self, mock_logs): + arglist = ['123', '--full'] + verifylist = [('uuid', '123'), ('full', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) diff --git a/validations_libs/tests/cli/test_list.py b/validations_libs/tests/cli/test_list.py new file mode 100644 index 00000000..6540c172 --- /dev/null +++ b/validations_libs/tests/cli/test_list.py @@ -0,0 +1,78 @@ +# 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.tests import fakes +from validations_libs.tests.cli.fakes import BaseCommand + + +class TestList(BaseCommand): + + def setUp(self): + super(TestList, self).setUp() + self.cmd = lister.ValidationList(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'list_validations', + return_value=fakes.VALIDATIONS_LIST) + def test_list_validations(self, mock_list): + arglist = ['--validation-dir', 'foo'] + verifylist = [('validation_dir', 'foo')] + + list = [{'description': 'My Validation One Description', + 'groups': ['prep', 'pre-deployment'], + 'id': 'my_val1', + 'name': 'My Validation One Name', + 'parameters': {} + }, { + 'description': 'My Validation Two Description', + 'groups': ['prep', 'pre-introspection'], + 'id': 'my_val2', + 'name': 'My Validation Two Name', + 'parameters': {'min_value': 8} + }] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertEqual(result, list) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'list_validations', + return_value=[]) + def test_list_validations_empty(self, mock_list): + arglist = ['--validation-dir', 'foo'] + verifylist = [('validation_dir', 'foo')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertEqual(result, []) + + @mock.patch('validations_libs.utils.parse_all_validations_on_disk', + return_value=fakes.VALIDATIONS_LIST_GROUP) + def test_list_validations_group(self, mock_list): + arglist = ['--validation-dir', 'foo', '--group', 'prep'] + verifylist = [('validation_dir', 'foo'), + ('group', ['prep'])] + + list = fakes.VALIDATION_LIST_RESULT + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertEqual(result, list) diff --git a/validations_libs/tests/cli/test_run.py b/validations_libs/tests/cli/test_run.py new file mode 100644 index 00000000..660f021a --- /dev/null +++ b/validations_libs/tests/cli/test_run.py @@ -0,0 +1,274 @@ +# 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 run +from validations_libs.tests import fakes +from validations_libs.tests.cli.fakes import BaseCommand + + +class TestRun(BaseCommand): + + def setUp(self): + super(TestRun, self).setUp() + self.cmd = run.Run(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=None) + def test_run_command_return_none(self, mock_run): + arglist = ['--validation', 'foo'] + verifylist = [('validation_name', ['foo'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertEqual(result, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + def test_run_command_success(self, mock_run): + arglist = ['--validation', 'foo'] + verifylist = [('validation_name', ['foo'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + def test_run_command_exclusive_group(self): + arglist = ['--validation', 'foo', '--group', 'bar'] + verifylist = [('validation_name', ['foo'], 'group', 'bar')] + + self.assertRaises(Exception, self.check_parser, self.cmd, + arglist, verifylist) + + @mock.patch('validations_libs.cli.common.print_dict') + @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_command_extra_vars(self, mock_run, mock_user, mock_print): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': {'key': 'value'}, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': None, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-vars', 'key=value'] + verifylist = [('validation_name', ['foo']), + ('extra_vars', {'key': 'value'})] + + 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.cli.common.print_dict') + @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_command_extra_vars_twice(self, mock_run, mock_user, + mock_print): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': {'key': 'value2'}, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': None, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-vars', 'key=value1', + '--extra-vars', 'key=value2'] + verifylist = [('validation_name', ['foo']), + ('extra_vars', {'key': 'value2'})] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + mock_run.assert_called_with(**run_called_args) + + def test_run_command_exclusive_vars(self): + arglist = ['--validation', 'foo', + '--extra-vars', 'key=value1', + '--extra-vars-file', '/foo/vars.yaml'] + verifylist = [('validation_name', ['foo']), + ('extra_vars', {'key': 'value2'})] + + self.assertRaises(Exception, self.check_parser, self.cmd, + arglist, verifylist) + + @mock.patch('yaml.safe_load', return_value={'key': 'value'}) + @mock.patch('six.moves.builtins.open') + @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_command_extra_vars_file(self, mock_run, mock_user, mock_open, + mock_yaml): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': {'key': 'value'}, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': None, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-vars-file', '/foo/vars.yaml'] + verifylist = [('validation_name', ['foo']), + ('extra_vars_file', '/foo/vars.yaml')] + + 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('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + def test_run_command_extra_env_vars(self, mock_run, mock_user): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': None, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': {'key': 'value'}, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-env-vars', 'key=value'] + verifylist = [('validation_name', ['foo']), + ('extra_env_vars', {'key': 'value'})] + + 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('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + def test_run_command_extra_env_vars_twice(self, mock_run, mock_user): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': None, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': {'key': 'value2'}, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-env-vars', 'key=value1', + '--extra-env-vars', 'key=value2'] + verifylist = [('validation_name', ['foo']), + ('extra_env_vars', {'key': 'value2'})] + + 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('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_SUCCESS_RUN) + def test_run_command_extra_env_vars_and_extra_vars(self, mock_run, + mock_user): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': {'key': 'value'}, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': {'key2': 'value2'}, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo', + '--extra-vars', 'key=value', + '--extra-env-vars', 'key2=value2'] + verifylist = [('validation_name', ['foo']), + ('extra_vars', {'key': 'value'}), + ('extra_env_vars', {'key2': 'value2'})] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + mock_run.assert_called_with(**run_called_args) + + def test_run_command_exclusive_wrong_extra_vars(self): + arglist = ['--validation', 'foo', + '--extra-vars', 'key=value1,key=value2'] + verifylist = [('validation_name', ['foo']), + ('extra_vars', {'key': 'value2'})] + + self.assertRaises(Exception, self.check_parser, self.cmd, + arglist, verifylist) + + @mock.patch('getpass.getuser', + return_value='doe') + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'run_validations', + return_value=fakes.FAKE_FAILED_RUN) + def test_run_command_failed_validation(self, mock_run, mock_user): + run_called_args = { + 'inventory': 'localhost', + 'limit_hosts': None, + 'group': [], + 'extra_vars': {'key': 'value'}, + 'validations_dir': '/usr/share/ansible/validation-playbooks', + 'base_dir': '/usr/share/ansible/', + 'validation_name': ['foo'], + 'extra_env_vars': {'key2': 'value2'}, + 'quiet': True, + 'ssh_user': 'doe'} + + arglist = ['--validation', 'foo'] + verifylist = [('validation_name', ['foo'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises(RuntimeError, self.cmd.take_action, parsed_args) diff --git a/validations_libs/tests/cli/test_show.py b/validations_libs/tests/cli/test_show.py new file mode 100644 index 00000000..adab8de9 --- /dev/null +++ b/validations_libs/tests/cli/test_show.py @@ -0,0 +1,79 @@ +# 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 show +from validations_libs.tests import fakes +from validations_libs.tests.cli.fakes import BaseCommand + + +class TestShow(BaseCommand): + + def setUp(self): + super(TestShow, self).setUp() + self.cmd = show.Show(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_validations') + def test_show_validations(self, mock_show): + arglist = ['foo'] + verifylist = [('validation_name', 'foo')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + +class TestShowGroup(BaseCommand): + + def setUp(self): + super(TestShowGroup, self).setUp() + self.cmd = show.ShowGroup(self.app, None) + + @mock.patch('yaml.safe_load', return_value=fakes.GROUP) + @mock.patch('six.moves.builtins.open') + def test_show_validations_group_info(self, mock_open, mock_yaml): + arglist = ['--group', 'group.yaml'] + verifylist = [('group', 'group.yaml')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + +class TestShowParameter(BaseCommand): + + def setUp(self): + super(TestShowParameter, self).setUp() + self.cmd = show.ShowParameter(self.app, None) + + @mock.patch('six.moves.builtins.open') + def test_show_validations_parameters_by_group(self, mock_open): + arglist = ['--group', 'prep'] + verifylist = [('group', ['prep'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + def test_show_parameter_exclusive_group(self): + arglist = ['--validation', 'foo', '--group', 'bar'] + verifylist = [('validation_name', ['foo'], 'group', ['bar'])] + + self.assertRaises(Exception, self.check_parser, self.cmd, + arglist, verifylist) + + @mock.patch('six.moves.builtins.open') + def test_show_validations_parameters_by_validations(self, mock_open): + arglist = ['--group', 'prep'] + verifylist = [('group', ['prep'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/validations_libs/tests/fakes.py b/validations_libs/tests/fakes.py index 2e459dd8..8353a2d7 100644 --- a/validations_libs/tests/fakes.py +++ b/validations_libs/tests/fakes.py @@ -27,6 +27,19 @@ VALIDATIONS_LIST = [{ 'parameters': {'min_value': 8} }] +VALIDATIONS_LIST_GROUP = [{ + 'description': 'My Validation Two Description', + 'groups': ['prep', 'pre-introspection'], + 'id': 'my_val2', + 'name': 'My Validation Two Name', + 'parameters': {'min_value': 8} +}] + + +VALIDATION_LIST_RESULT = (('ID', 'Name', 'Groups'), + [('my_val2', 'My Validation Two Name', + ['prep', 'pre-introspection'])]) + GROUPS_LIST = [ ('group1', 'Group1 description'), ('group2', 'Group2 description'), @@ -252,6 +265,36 @@ GROUP = {'no-op': [{'description': 'noop-foo'}], 'pre': [{'description': 'pre-foo'}], 'post': [{'description': 'post-foo'}]} +FAKE_SUCCESS_RUN = [{'Duration': '0:00:01.761', + 'Host_Group': 'overcloud', + 'Status': 'PASSED', + 'Status_by_Host': 'subnode-1,PASSED, subnode-2,PASSED', + 'UUID': '123', + 'Unreachable_Hosts': '', + 'Validations': 'foo'}] + +FAKE_FAILED_RUN = [{'Duration': '0:00:01.761', + 'Host_Group': 'overcloud', + 'Status': 'FAILED', + 'Status_by_Host': 'subnode-1,FAILED, subnode-2,PASSED', + 'UUID': '123', + 'Unreachable_Hosts': '', + 'Validations': 'foo'}, + {'Duration': '0:00:01.761', + 'Host_Group': 'overcloud', + 'Status': 'FAILED', + 'Status_by_Host': 'subnode-1,FAILED, subnode-2,PASSED', + 'UUID': '123', + 'Unreachable_Hosts': '', + 'Validations': 'foo'}, + {'Duration': '0:00:01.761', + 'Host_Group': 'overcloud', + 'Status': 'PASSED', + 'Status_by_Host': 'subnode-1,PASSED, subnode-2,PASSED', + 'UUID': '123', + 'Unreachable_Hosts': '', + 'Validations': 'foo'}] + def fake_ansible_runner_run_return(status='successful', rc=0): return status, rc diff --git a/validations_libs/tests/test_validation_actions.py b/validations_libs/tests/test_validation_actions.py index 24e1531c..58d7bf3c 100644 --- a/validations_libs/tests/test_validation_actions.py +++ b/validations_libs/tests/test_validation_actions.py @@ -308,8 +308,7 @@ class TestValidationActions(TestCase): {'parameters': fakes.FAKE_METADATA}} v_actions = ValidationActions() result = v_actions.show_validations_parameters('foo') - self.assertEqual(result, json.dumps(mock_get_param.return_value, - indent=4, sort_keys=True)) + self.assertEqual(result, mock_get_param.return_value) @mock.patch('six.moves.builtins.open') def test_show_validations_parameters_non_supported_format(self, mock_open): diff --git a/validations_libs/validation_actions.py b/validations_libs/validation_actions.py index 2a6a209a..b7738c93 100644 --- a/validations_libs/validation_actions.py +++ b/validations_libs/validation_actions.py @@ -44,9 +44,8 @@ class ValidationActions(object): self.log = logging.getLogger(__name__ + ".ValidationActions") self.validation_path = (validation_path if validation_path else constants.ANSIBLE_VALIDATION_DIR) - self.group = group - def list_validations(self): + def list_validations(self, group=None): """Get a list of the available validations This is used to print table from python ``Tuple`` with ``PrettyTable``. @@ -76,7 +75,7 @@ class ValidationActions(object): """ self.log = logging.getLogger(__name__ + ".list_validations") validations = v_utils.parse_all_validations_on_disk( - self.validation_path, self.group) + self.validation_path, group) return_values = [] column_name = ('ID', 'Name', 'Groups') @@ -498,15 +497,7 @@ class ValidationActions(object): allow_unicode=True, default_flow_style=False, indent=2)) - if output_format == 'json': - return json.dumps(params, - indent=4, - sort_keys=True) - else: - return yaml.safe_dump(params, - allow_unicode=True, - default_flow_style=False, - indent=2) + return params def show_history(self, validation_ids=None, extension='json', log_path=constants.VALIDATIONS_LOG_BASEDIR):