Create dedicated CLI for the Validation Framework

This review adds a CLI for the Validation Framework which will become
the only entry point to run the Validation Framework.

It will deprecate the tripleo_validatior.py CLI and the
validation.py script.

This patch uses python cliff library to provide nice helpers, shell
and output formats.

Change-Id: I66800ad51cc50f4eb37efabe85fb553dce008101
This commit is contained in:
matbu 2021-03-23 18:33:04 +01:00
parent bb23468ee5
commit 5fad1a4d18
19 changed files with 1283 additions and 14 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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:]))

View File

@ -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))

View File

@ -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="<validation>",
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="<uuid>",
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)

View File

@ -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='<group>[,<group>,...]',
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))

View File

@ -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))

174
validations_libs/cli/run.py Normal file
View File

@ -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=<val1> [--extra-vars key3=<val3>]",
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=<val1> [--extra-vars key3=<val3>]",
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='<validation_id>[,<validation_id>,...]',
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='<group>[,<group>,...]',
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.")

View File

@ -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="<validation>",
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='<group_name>',
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='<validation_id>[,<validation_id>,...]',
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='<group_id>[,<group_id>,...]',
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='<format_output>',
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()

View File

@ -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.
#

View File

@ -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()

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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):