From b0f9444b770a359f4cdc62e104b3f45bab22f8f2 Mon Sep 17 00:00:00 2001 From: Mathieu Bultel Date: Fri, 11 Dec 2020 22:50:08 +0100 Subject: [PATCH] Move tripleo validations CLI from tripleoclient to tripleo-validations repo This patch is moving the CLI outside of tripleoclient to tripleo-validations repository as an additional tool on top of the main tripleo CLI. Change-Id: I3340fb3b038f1f081bddb26468e4aea9b1ab6883 --- setup.cfg | 13 + tests/test_tripleo_validator.py | 279 +++++++++++++ tripleo_validations/constants.py | 35 ++ tripleo_validations/plugin.py | 55 +++ tripleo_validations/tripleo_validator.py | 494 +++++++++++++++++++++++ 5 files changed, 876 insertions(+) create mode 100644 tests/test_tripleo_validator.py create mode 100644 tripleo_validations/constants.py create mode 100644 tripleo_validations/plugin.py create mode 100644 tripleo_validations/tripleo_validator.py diff --git a/setup.cfg b/setup.cfg index 1549e1228..1e49693f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,3 +38,16 @@ data_files = share/ansible/callback_plugins = callback_plugins/* share/ansible/lookup_plugins = lookup_plugins/* share/ansible/library = library/* + +[entry_points] +openstack.cli.extension = + tripleo_validations = tripleo_validations.plugin + +openstack.tripleo_validations.v2 = + tripleo_validator_group_info = tripleo_validations.tripleo_validator:TripleOValidatorGroupInfo + tripleo_validator_list = tripleo_validations.tripleo_validator:TripleOValidatorList + tripleo_validator_run = tripleo_validations.tripleo_validator:TripleOValidatorRun + tripleo_validator_show = tripleo_validations.tripleo_validator:TripleOValidatorShow + tripleo_validator_show_history = tripleo_validations.tripleo_validator:TripleOValidatorShowHistory + tripleo_validator_show_parameter = tripleo_validations.tripleo_validator:TripleOValidatorShowParameter + tripleo_validator_show_run = tripleo_validations.tripleo_validator:TripleOValidatorShowRun diff --git a/tests/test_tripleo_validator.py b/tests/test_tripleo_validator.py new file mode 100644 index 000000000..2b203470a --- /dev/null +++ b/tests/test_tripleo_validator.py @@ -0,0 +1,279 @@ +# Copyright 2018 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 mock + +from osc_lib.tests import utils +from tripleo_validations import tripleo_validator + +VALIDATIONS_LIST = [{ + 'description': 'My Validation One Description', + 'groups': ['prep', 'pre-deployment'], + 'id': 'my_val1', + 'name': 'My Validition One Name', + 'parameters': {} +}, { + 'description': 'My Validation Two Description', + 'groups': ['prep', 'pre-introspection'], + 'id': 'my_val2', + 'name': 'My Validition Two Name', + 'parameters': {'min_value': 8} +}] + +GROUPS_LIST = [ + ('group1', 'Group1 description'), + ('group2', 'Group2 description'), + ('group3', 'Group3 description'), +] + +VALIDATIONS_LOGS_CONTENTS_LIST = [{ + 'plays': [{ + 'play': { + 'duration': { + 'end': '2019-11-25T13:40:17.538611Z', + 'start': '2019-11-25T13:40:14.404623Z', + 'time_elapsed': '0:00:03.753' + }, + 'host': 'undercloud', + 'id': '008886df-d297-1eaa-2a74-000000000008', + 'validation_id': '512e', + 'validation_path': + '/usr/share/ansible/validation-playbooks' + }, + 'tasks': [ + { + 'hosts': { + 'undercloud': { + '_ansible_no_log': False, + 'action': 'command', + 'changed': False, + 'cmd': [u'ls', '/sys/class/block/'], + 'delta': '0:00:00.018913', + 'end': '2019-11-25 13:40:17.120368', + 'invocation': { + 'module_args': { + '_raw_params': 'ls /sys/class/block/', + '_uses_shell': False, + 'argv': None, + 'chdir': None, + 'creates': None, + 'executable': None, + 'removes': None, + 'stdin': None, + 'stdin_add_newline': True, + 'strip_empty_ends': True, + 'warn': True + } + }, + 'rc': 0, + 'start': '2019-11-25 13:40:17.101455', + 'stderr': '', + 'stderr_lines': [], + 'stdout': 'vda', + 'stdout_lines': [u'vda'] + } + }, + 'task': { + 'duration': { + 'end': '2019-11-25T13:40:17.336687Z', + 'start': '2019-11-25T13:40:14.529880Z' + }, + 'id': + '008886df-d297-1eaa-2a74-00000000000d', + 'name': + 'advanced-format-512e-support : List the available drives' + } + }, + { + 'hosts': { + 'undercloud': { + 'action': + 'advanced_format', + 'changed': False, + 'msg': + 'All items completed', + 'results': [{ + '_ansible_item_label': 'vda', + '_ansible_no_log': False, + 'ansible_loop_var': 'item', + 'changed': False, + 'item': 'vda', + 'skip_reason': 'Conditional result was False', + 'skipped': True + }], + 'skipped': True + } + }, + 'task': { + 'duration': { + 'end': '2019-11-25T13:40:17.538611Z', + 'start': '2019-11-25T13:40:17.341704Z' + }, + 'id': '008886df-d297-1eaa-2a74-00000000000e', + 'name': + 'advanced-format-512e-support: Detect the drive' + } + } + ] + }], + 'stats': { + 'undercloud': { + 'changed': 0, + 'failures': 0, + 'ignored': 0, + 'ok': 1, + 'rescued': 0, + 'skipped': 1, + 'unreachable': 0 + } + }, + 'validation_output': [] +}] + + +class TestValidatorGroupInfo(utils.TestCommand): + + def setUp(self): + super(TestValidatorGroupInfo, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorGroupInfo(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'group_information', return_value=GROUPS_LIST) + def test_show_group_info(self, mock_validations): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + +class TestValidatorList(utils.TestCommand): + + def setUp(self): + super(TestValidatorList, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorList(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'list_validations', + return_value=VALIDATIONS_LIST) + def test_validation_list_noargs(self, mock_validations): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + +class TestValidatorShow(utils.TestCommand): + + def setUp(self): + super(TestValidatorShow, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorShow(self.app, None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_validations', + return_value=VALIDATIONS_LIST[0]) + def test_validation_show(self, mock_validations): + arglist = ['my_val1'] + verifylist = [('validation_id', 'my_val1')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + +class TestValidatorShowParameter(utils.TestCommand): + + def setUp(self): + super(TestValidatorShowParameter, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorShowParameter(self.app, + None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_validations_parameters', + return_value=VALIDATIONS_LIST[1]) + def test_validation_show_parameter(self, mock_validations): + arglist = ['--validation', 'my_val2'] + verifylist = [('validation_name', ['my_val2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + +class TestValidatorShowRun(utils.TestCommand): + + def setUp(self): + super(TestValidatorShowRun, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorShowRun(self.app, + None) + + @mock.patch('validations_libs.validation_actions.ValidationLogs.' + 'get_logfile_content_by_uuid', + return_value=VALIDATIONS_LOGS_CONTENTS_LIST) + def test_validation_show_run(self, mock_validations): + arglist = ['008886df-d297-1eaa-2a74-000000000008'] + verifylist = [('uuid', '008886df-d297-1eaa-2a74-000000000008')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + +class TestValidatorShowHistory(utils.TestCommand): + + def setUp(self): + super(TestValidatorShowHistory, self).setUp() + + # Get the command object to test + self.cmd = tripleo_validator.TripleOValidatorShowHistory(self.app, + None) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_history', + return_value=VALIDATIONS_LOGS_CONTENTS_LIST) + def test_validation_show_history(self, mock_validations): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + @mock.patch('validations_libs.validation_actions.ValidationActions.' + 'show_history', + return_value=VALIDATIONS_LOGS_CONTENTS_LIST) + def test_validation_show_history_for_a_validation(self, mock_validations): + arglist = [ + '--validation', + '512e' + ] + verifylist = [('validation', '512e')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) diff --git a/tripleo_validations/constants.py b/tripleo_validations/constants.py new file mode 100644 index 000000000..cd8a87718 --- /dev/null +++ b/tripleo_validations/constants.py @@ -0,0 +1,35 @@ +# Copyright 2015 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 os + +DEFAULT_VALIDATIONS_BASEDIR = "/usr/share/ansible" +DEFAULT_VALIDATIONS_LEGACY_BASEDIR = "/usr/share/openstack-tripleo-validations" + +VALIDATIONS_LOG_BASEDIR = '/var/log/validations' + +ANSIBLE_VALIDATION_DIR = ( + os.path.join(DEFAULT_VALIDATIONS_LEGACY_BASEDIR, 'playbooks') + if os.path.exists(os.path.join(DEFAULT_VALIDATIONS_LEGACY_BASEDIR, + 'playbooks')) + else "/usr/share/ansible/validation-playbooks" + ) + + +VALIDATION_GROUPS_INFO = ( + '/usr/share/ansible/groups.yaml' + if os.path.exists('/usr/share/ansible/groups.yaml') + else os.path.join(DEFAULT_VALIDATIONS_LEGACY_BASEDIR, 'groups.yaml') + ) diff --git a/tripleo_validations/plugin.py b/tripleo_validations/plugin.py new file mode 100644 index 000000000..c67c04625 --- /dev/null +++ b/tripleo_validations/plugin.py @@ -0,0 +1,55 @@ +# Copyright 2013 Nebula 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. +# + +"""OpenStackClient Plugin interface""" + +import logging + +from osc_lib import utils + + +LOG = logging.getLogger(__name__) + +DEFAULT_TRIPLEO_VALIDATION_API_VERSION = '2' + +# Required by the OSC plugin interface +API_NAME = 'tripleo_validations' +API_VERSION_OPTION = 'os_tripleo_validations_api_version' +API_VERSIONS = { + '2': 'tripleo_validations.plugin' +} + + +# Required by the OSC plugin interface +def build_option_parser(parser): + """Hook to add global options + + Called from openstackclient.shell.OpenStackShell.__init__() + after the builtin parser has been initialized. This is + where a plugin can add global options such as an API version setting. + + :param argparse.ArgumentParser parser: The parser object that has been + initialized by OpenStackShell. + """ + parser.add_argument( + '--os-tripleo-validations-api-version', + metavar='', + default=utils.env( + 'OS_TRIPLEO_VALIDATIONS_API_VERSION', + default=DEFAULT_TRIPLEO_VALIDATION_API_VERSION), + help=("TripleO Client API version, default={}" + " (Env: DEFAULT_TRIPLEO_VALIDATION_API_VERSION)").format( + DEFAULT_TRIPLEO_VALIDATION_API_VERSION)) + return parser diff --git a/tripleo_validations/tripleo_validator.py b/tripleo_validations/tripleo_validator.py new file mode 100644 index 000000000..0169931dc --- /dev/null +++ b/tripleo_validations/tripleo_validator.py @@ -0,0 +1,494 @@ +# Copyright 2019 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 +import json +import logging +import yaml + +from openstack import exceptions as os_exceptions +from osc_lib import exceptions +from osc_lib.i18n import _ +from prettytable import PrettyTable + +from osc_lib.command import command +from tripleoclient import utils as oooutils +from tripleoclient.workflows import deployment +from tripleo_validations import constants + +from validations_libs import constants as v_consts +from validations_libs import utils as v_utils +from validations_libs.validation_actions import ValidationActions +from validations_libs.validation_logs import ValidationLogs + +LOG = logging.getLogger(__name__ + ".TripleoValidator") + +RED = "\033[1;31m" +GREEN = "\033[0;32m" +CYAN = "\033[36m" +YELLOW = "\033[0;33m" +RESET = "\033[0;0m" + +FAILED_VALIDATION = "{}FAILED{}".format(RED, RESET) +PASSED_VALIDATION = "{}PASSED{}".format(GREEN, RESET) + +GROUP_FILE = constants.VALIDATION_GROUPS_INFO + +NO_VALIDATION_STATE = ['DEPLOY_FAILED', 'DEPLOYING'] + + +class _CommaListGroupAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + opts = v_utils.get_validation_group_name_list(GROUP_FILE) + for value in values.split(','): + if value not in opts: + message = ("Invalid choice: {value} (choose from {choice})" + .format(value=value, + choice=opts)) + raise argparse.ArgumentError(self, message) + setattr(namespace, self.dest, values.split(',')) + + +class _CommaListAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + setattr(namespace, self.dest, values.split(',')) + + +class TripleOValidatorGroupInfo(command.Lister): + """Display Information about Validation Groups""" + + auth_required = False + + def get_parser(self, prog_name): + parser = super(TripleOValidatorGroupInfo, self).get_parser(prog_name) + return parser + + def take_action(self, parsed_args): + actions = ValidationActions(constants.ANSIBLE_VALIDATION_DIR) + return actions.group_information(GROUP_FILE) + + +class TripleOValidatorShow(command.ShowOne): + """Display detailed information about a Validation""" + + auth_required = False + + def get_parser(self, prog_name): + parser = super(TripleOValidatorShow, self).get_parser(prog_name) + + parser.add_argument('validation_id', + metavar="", + type=str, + help='Validation ID') + + return parser + + def take_action(self, parsed_args): + LOG.debug(_('Show validation result')) + actions = ValidationActions(constants.ANSIBLE_VALIDATION_DIR) + + try: + data = actions.show_validations(parsed_args.validation_id) + except Exception as e: + raise exceptions.CommandError(e) + + if data: + return data.keys(), data.values() + + +class TripleOValidatorShowParameter(command.Command): + """Display Validations Parameters""" + + auth_required = False + + def get_parser(self, prog_name): + parser = argparse.ArgumentParser( + description=self.get_description(), + prog=prog_name, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=True + ) + + 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', + metavar='[,,...]', + action=_CommaListGroupAction, + 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( + '-f', '--format', + 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): + actions = ValidationActions(constants.ANSIBLE_VALIDATION_DIR) + params = actions.show_validations_parameters( + parsed_args.validation_name, + parsed_args.group, + parsed_args.format, + parsed_args.download) + if parsed_args.download: + print("The file {} has been created successfully".format( + parsed_args.download)) + else: + print(params) + + +class TripleOValidatorList(command.Lister): + """List the available validations""" + + auth_required = False + + def get_parser(self, prog_name): + parser = super(TripleOValidatorList, self).get_parser(prog_name) + + parser.add_argument( + '--group', + metavar='[,,...]', + action=_CommaListGroupAction, + default=[], + help=_("List 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): + LOG.debug(_('Launch listing the validations')) + try: + v_consts.DEFAULT_VALIDATIONS_BASEDIR = constants.\ + DEFAULT_VALIDATIONS_BASEDIR + actions = ValidationActions(constants.ANSIBLE_VALIDATION_DIR, + parsed_args.group) + return actions.list_validations() + except Exception as e: + raise RuntimeError(_("Validations listing finished with errors\n" + "Output: {}").format(e)) + + +class TripleOValidatorRun(command.Command): + """Run the available validations""" + + auth_required = False + + def get_parser(self, prog_name): + parser = argparse.ArgumentParser( + description=self.get_description(), + prog=prog_name, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=False + ) + + parser.add_argument( + '--plan', '--stack', + dest='plan', + default=None, + help=_("Execute the validations using a custom plan name") + ) + + parser.add_argument( + '--ssh-user', + dest='ssh_user', + default='heat-admin', + help=_("Ssh User name for the Overcloud ssh connection.") + ) + + 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\".") + ) + extra_vars_group = parser.add_mutually_exclusive_group(required=False) + + extra_vars_group.add_argument( + '--extra-vars', + action='store', + default={}, + type=json.loads, + help=_( + "Add a dictionary as extra variable to a validation: " + "--extra-vars '{\"min_undercloud_ram_gb\": 24}'") + ) + + extra_vars_group.add_argument( + '--extra-vars-file', + action='store', + default='', + help=_( + "Add a JSON/YAML file containing extra variable " + "to a validation: " + "--extra-vars-file /home/stack/vars.[json|yaml] " + "If using Mistral, only a valid JSON file will be " + "supported." + ) + ) + + extra_vars_group.add_argument( + '--extra-env-vars', + action='store', + default={}, + type=json.loads, + help=_( + "A dictionary as extra environment variables you may need " + "to provide to your Ansible execution example:" + "ANSIBLE_STDOUT_CALLBACK=default") + ) + + extra_vars_group.add_argument( + '--static-inventory', + action='store', + default='', + help=_( + "Provide your own static inventory file. You can generate " + "such an inventory calling tripleo-ansible-inventory command. " + "Especially useful when heat service isn't available." + ) + ) + + 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', + metavar='[,,...]', + action=_CommaListGroupAction, + 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 _run_validator_run(self, parsed_args): + LOG = logging.getLogger(__name__ + ".ValidationsRunAnsible") + + plan = parsed_args.plan + # Try to perform OpenStack authentication, if no authentication + # and static inventory provided continue, else raise error. + try: + clients = self.app.client_manager + clients._auth_required = True + clients.setup_auth() + except os_exceptions.ConfigException: + msg = "Running Validations without authentication." + LOG.warning("{}{}{}".format(YELLOW, msg, RESET)) + if not parsed_args.static_inventory: + raise exceptions.CommandError( + _("No static inventory provided, please provide a valid " + "inventory or use authentication.")) + else: + if plan: + status = deployment.get_deployment_status(clients, plan) + if not status or status in NO_VALIDATION_STATE: + raise exceptions.CommandError( + _("The plan and the stack '{}' doesn't exist OR are " + "in 'failed' or 'deploying' state." + "Please use a valid plan".format(plan))) + else: + msg = "Running Validations without Overcloud settings." + LOG.warning("{}{}{}".format(YELLOW, msg, RESET)) + limit = parsed_args.limit + 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.update(yaml.safe_load(env_file.read())) + except yaml.YAMLError as e: + error_msg = ( + "The request body must be properly formatted YAML/JSON. " + "Details: %s." % e) + raise exceptions.CommandError(error_msg) + + # We don't check if the file exists in order to support + # passing a string such as "localhost,", like we can do with + # the "-i" option of ansible-playbook. + if parsed_args.static_inventory: + static_inventory = parsed_args.static_inventory + else: + static_inventory = oooutils.get_tripleo_ansible_inventory( + ssh_user=parsed_args.ssh_user, + stack=parsed_args.plan, + undercloud_connection='local', + return_inventory_file_path=True) + + v_consts.DEFAULT_VALIDATIONS_BASEDIR = constants.\ + DEFAULT_VALIDATIONS_BASEDIR + actions = ValidationActions() + try: + results = actions.run_validations( + inventory=static_inventory, + limit_hosts=limit, + group=parsed_args.group, + extra_vars=extra_vars, + validations_dir=constants.ANSIBLE_VALIDATION_DIR, + validation_name=parsed_args.validation_name, + extra_env_vars=parsed_args.extra_env_vars, + quiet=True) + except RuntimeError as e: + raise exceptions.CommandError(e) + + if results: + # Build output + t = PrettyTable(border=True, header=True, padding_width=1) + # Set Field name by getting the result dict keys + t.field_names = results[0].keys() + for r in results: + if r.get('Status_by_Host'): + h = [] + for host in r['Status_by_Host'].split(', '): + _name, _status = host.split(',') + color = (GREEN if _status == 'PASSED' else RED) + _name = '{}{}{}'.format(color, _name, RESET) + h.append(_name) + r['Status_by_Host'] = ', '.join(h) + if r.get('status'): + status = r.get('status') + color = (CYAN if status in ['starting', 'running'] + else GREEN if status == 'PASSED' else RED) + r['status'] = '{}{}{}'.format(color, status, RESET) + t.add_row(r.values()) + print(t) + else: + msg = "No Validation has been run, please check your parameters." + raise exceptions.CommandError(msg) + + if not parsed_args.static_inventory: + LOG.debug(_('Removing static tripleo ansible inventory file')) + oooutils.cleanup_tripleo_ansible_inventory_file( + static_inventory) + + def take_action(self, parsed_args): + self._run_validator_run(parsed_args) + + +class TripleOValidatorShowRun(command.Command): + """Display details about a Validation execution""" + + auth_required = False + + def get_parser(self, prog_name): + parser = argparse.ArgumentParser( + description=self.get_description(), + prog=prog_name, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + add_help=False + ) + + 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') + + return parser + + def take_action(self, parsed_args): + vlogs = ValidationLogs() + 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 exceptions.CommandError( + "Could not find the log file linked to this UUID: %s" % + parsed_args.uuid) + + +class TripleOValidatorShowHistory(command.Lister): + """Display Validations execution history""" + + auth_required = False + + def get_parser(self, prog_name): + parser = super(TripleOValidatorShowHistory, self).get_parser(prog_name) + + parser.add_argument('--validation', + metavar="", + type=str, + help='Display execution history for a validation') + + return parser + + def take_action(self, parsed_args): + actions = ValidationActions(constants.ANSIBLE_VALIDATION_DIR) + return actions.show_history(parsed_args.validation)