Merge "Add the community validation paths"

This commit is contained in:
Zuul 2021-11-18 19:39:23 +00:00 committed by Gerrit Code Review
commit 1b94c03ee7
10 changed files with 386 additions and 70 deletions

View File

@ -126,8 +126,16 @@ class Ansible(object):
gathering_policy, module_path, key, gathering_policy, module_path, key,
extra_env_variables, ansible_timeout, extra_env_variables, ansible_timeout,
callback_whitelist, base_dir, python_interpreter, callback_whitelist, base_dir, python_interpreter,
env={}): env={}, validation_cfg_file=None):
"""Handle Ansible env var for Ansible config execution""" """Handle Ansible env var for Ansible config execution"""
community_roles = ""
community_library = ""
community_lookup = ""
if utils.community_validations_on(validation_cfg_file):
community_roles = f"{constants.COMMUNITY_ROLES_DIR}:"
community_library = f"{constants.COMMUNITY_LIBRARY_DIR}:"
community_lookup = f"{constants.COMMUNITY_LOOKUP_DIR}:"
cwd = os.getcwd() cwd = os.getcwd()
env['ANSIBLE_SSH_ARGS'] = ( env['ANSIBLE_SSH_ARGS'] = (
'-o UserKnownHostsFile={} ' '-o UserKnownHostsFile={} '
@ -159,10 +167,12 @@ class Ansible(object):
'{}:{}:' '{}:{}:'
'/usr/share/ansible/plugins/modules:' '/usr/share/ansible/plugins/modules:'
'/usr/share/ceph-ansible/library:' '/usr/share/ceph-ansible/library:'
'{community_path}'
'{}/library'.format( '{}/library'.format(
os.path.join(workdir, 'modules'), os.path.join(workdir, 'modules'),
os.path.join(cwd, 'modules'), os.path.join(cwd, 'modules'),
base_dir base_dir,
community_path=community_library
) )
) )
env['ANSIBLE_LOOKUP_PLUGINS'] = os.path.expanduser( env['ANSIBLE_LOOKUP_PLUGINS'] = os.path.expanduser(
@ -170,10 +180,12 @@ class Ansible(object):
'{}:{}:' '{}:{}:'
'/usr/share/ansible/plugins/lookup:' '/usr/share/ansible/plugins/lookup:'
'/usr/share/ceph-ansible/plugins/lookup:' '/usr/share/ceph-ansible/plugins/lookup:'
'{community_path}'
'{}/lookup_plugins'.format( '{}/lookup_plugins'.format(
os.path.join(workdir, 'lookup'), os.path.join(workdir, 'lookup'),
os.path.join(cwd, 'lookup'), os.path.join(cwd, 'lookup'),
base_dir base_dir,
community_path=community_lookup
) )
) )
env['ANSIBLE_CALLBACK_PLUGINS'] = os.path.expanduser( env['ANSIBLE_CALLBACK_PLUGINS'] = os.path.expanduser(
@ -215,10 +227,12 @@ class Ansible(object):
'/usr/share/ansible/roles:' '/usr/share/ansible/roles:'
'/usr/share/ceph-ansible/roles:' '/usr/share/ceph-ansible/roles:'
'/etc/ansible/roles:' '/etc/ansible/roles:'
'{community_path}'
'{}/roles'.format( '{}/roles'.format(
os.path.join(workdir, 'roles'), os.path.join(workdir, 'roles'),
os.path.join(cwd, 'roles'), os.path.join(cwd, 'roles'),
base_dir base_dir,
community_path=community_roles
) )
) )
env['ANSIBLE_CALLBACK_WHITELIST'] = callback_whitelist env['ANSIBLE_CALLBACK_WHITELIST'] = callback_whitelist
@ -421,7 +435,8 @@ class Ansible(object):
connection, gathering_policy, connection, gathering_policy,
module_path, key, extra_env_variables, module_path, key, extra_env_variables,
ansible_timeout, callback_whitelist, ansible_timeout, callback_whitelist,
base_dir, python_interpreter)) base_dir, python_interpreter,
validation_cfg_file=validation_cfg_file))
if 'ANSIBLE_CONFIG' not in env and not ansible_cfg_file: if 'ANSIBLE_CONFIG' not in env and not ansible_cfg_file:
ansible_cfg_file = os.path.join(ansible_artifact_path, ansible_cfg_file = os.path.join(ansible_artifact_path,

View File

@ -67,4 +67,5 @@ class ValidationList(BaseLister):
v_actions = ValidationActions(validation_path=validation_dir) v_actions = ValidationActions(validation_path=validation_dir)
return (v_actions.list_validations(groups=group, return (v_actions.list_validations(groups=group,
categories=category, categories=category,
products=product)) products=product,
validation_config=self.base.config))

View File

@ -45,7 +45,8 @@ class Show(BaseShow):
validation_name = parsed_args.validation_name validation_name = parsed_args.validation_name
v_actions = ValidationActions(validation_path=validation_dir) v_actions = ValidationActions(validation_path=validation_dir)
data = v_actions.show_validations(validation_name) data = v_actions.show_validations(
validation_name, validation_config=self.base.config)
if data: if data:
return data.keys(), data.values() return data.keys(), data.values()
@ -70,7 +71,9 @@ class ShowGroup(BaseLister):
self.base.set_argument_parser(self, parsed_args) self.base.set_argument_parser(self, parsed_args)
v_actions = ValidationActions(parsed_args.validation_dir) v_actions = ValidationActions(parsed_args.validation_dir)
return v_actions.group_information(constants.VALIDATION_GROUPS_INFO) return v_actions.group_information(
constants.VALIDATION_GROUPS_INFO,
validation_config=self.base.config)
class ShowParameter(BaseShow): class ShowParameter(BaseShow):
@ -162,7 +165,8 @@ class ShowParameter(BaseShow):
categories=parsed_args.category, categories=parsed_args.category,
products=parsed_args.product, products=parsed_args.product,
output_format=parsed_args.format_output, output_format=parsed_args.format_output,
download_file=parsed_args.download) download_file=parsed_args.download,
validation_config=self.base.config)
if parsed_args.download: if parsed_args.download:
self.app.LOG.info( self.app.LOG.info(

View File

@ -248,7 +248,8 @@ FAKE_PLAYBOOK = [{'hosts': 'undercloud',
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'], 'products': ['product1'],
'name': 'name':
'Advanced Format 512e Support'}}}] 'Advanced Format 512e Support',
'path': '/tmp'}}}]
FAKE_PLAYBOOK2 = [{'hosts': 'undercloud', FAKE_PLAYBOOK2 = [{'hosts': 'undercloud',
'roles': ['advanced_format_512e_support'], 'roles': ['advanced_format_512e_support'],
@ -274,14 +275,16 @@ FAKE_METADATA = {'id': 'foo',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'], 'products': ['product1'],
'name': 'Advanced Format 512e Support'} 'name': 'Advanced Format 512e Support',
'path': '/tmp'}
FORMATED_DATA = {'Description': 'foo', FORMATED_DATA = {'Description': 'foo',
'Groups': ['prep', 'pre-deployment'], 'Groups': ['prep', 'pre-deployment'],
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'], 'Products': ['product1'],
'ID': 'foo', 'ID': 'foo',
'Name': 'Advanced Format 512e Support'} 'Name': 'Advanced Format 512e Support',
'Path': '/tmp'}
GROUP = {'no-op': [{'description': 'noop-foo'}], GROUP = {'no-op': [{'description': 'noop-foo'}],
'pre': [{'description': 'pre-foo'}], 'pre': [{'description': 'pre-foo'}],

View File

@ -21,6 +21,7 @@ except ImportError:
from unittest import TestCase from unittest import TestCase
from ansible_runner import Runner from ansible_runner import Runner
from validations_libs import constants
from validations_libs.ansible import Ansible from validations_libs.ansible import Ansible
from validations_libs.tests import fakes from validations_libs.tests import fakes
@ -175,6 +176,56 @@ class TestAnsible(TestCase):
'/tmp/foo/fact_cache' '/tmp/foo/fact_cache'
)) ))
def test_ansible_env_var_with_community_validations(self):
# AP No config file (use the default True)
env = self.run._ansible_env_var(
output_callback="", ssh_user="", workdir="", connection="",
gathering_policy="", module_path="", key="",
extra_env_variables="", ansible_timeout="",
callback_whitelist="", base_dir="", python_interpreter="",
env={}, validation_cfg_file=None)
assert(f"{constants.COMMUNITY_LIBRARY_DIR}:" in env["ANSIBLE_LIBRARY"])
assert(f"{constants.COMMUNITY_ROLES_DIR}:" in env["ANSIBLE_ROLES_PATH"])
assert(f"{constants.COMMUNITY_LOOKUP_DIR}:" in env["ANSIBLE_LOOKUP_PLUGINS"])
# AP config file with no settting (use the default True)
env = self.run._ansible_env_var(
output_callback="", ssh_user="", workdir="", connection="",
gathering_policy="", module_path="", key="",
extra_env_variables="", ansible_timeout="",
callback_whitelist="", base_dir="", python_interpreter="",
env={}, validation_cfg_file={"default": {}})
assert(f"{constants.COMMUNITY_LIBRARY_DIR}:" in env["ANSIBLE_LIBRARY"])
assert(f"{constants.COMMUNITY_ROLES_DIR}:" in env["ANSIBLE_ROLES_PATH"])
assert(f"{constants.COMMUNITY_LOOKUP_DIR}:" in env["ANSIBLE_LOOKUP_PLUGINS"])
# AP config file with settting True
env = self.run._ansible_env_var(
output_callback="", ssh_user="", workdir="", connection="",
gathering_policy="", module_path="", key="",
extra_env_variables="", ansible_timeout="",
callback_whitelist="", base_dir="", python_interpreter="",
env={}, validation_cfg_file={"default": {"enable_community_validations": True}})
assert(f"{constants.COMMUNITY_LIBRARY_DIR}:" in env["ANSIBLE_LIBRARY"])
assert(f"{constants.COMMUNITY_ROLES_DIR}:" in env["ANSIBLE_ROLES_PATH"])
assert(f"{constants.COMMUNITY_LOOKUP_DIR}:" in env["ANSIBLE_LOOKUP_PLUGINS"])
def test_ansible_env_var_without_community_validations(self):
# AP config file with settting False
env = self.run._ansible_env_var(
output_callback="", ssh_user="", workdir="", connection="",
gathering_policy="", module_path="", key="",
extra_env_variables="", ansible_timeout="",
callback_whitelist="", base_dir="", python_interpreter="",
env={}, validation_cfg_file={"default": {"enable_community_validations": False}})
assert(f"{constants.COMMUNITY_LIBRARY_DIR}:" not in env["ANSIBLE_LIBRARY"])
assert(f"{constants.COMMUNITY_ROLES_DIR}:" not in env["ANSIBLE_ROLES_PATH"])
assert(f"{constants.COMMUNITY_LOOKUP_DIR}:" not in env["ANSIBLE_LOOKUP_PLUGINS"])
def test_get_extra_vars_dict(self): def test_get_extra_vars_dict(self):
extra_vars = { extra_vars = {
'foo': 'bar' 'foo': 'bar'

View File

@ -44,10 +44,48 @@ class TestUtils(TestCase):
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'], 'Products': ['product1'],
'ID': '512e', 'ID': '512e',
'Parameters': {}} 'Parameters': {},
'Path': '/tmp'}
res = utils.get_validations_data('512e') res = utils.get_validations_data('512e')
self.assertEqual(res, output) self.assertEqual(res, output)
@mock.patch('validations_libs.validation.Validation._get_content',
return_value=fakes.FAKE_PLAYBOOK[0])
@mock.patch('six.moves.builtins.open')
@mock.patch('os.path.exists', side_effect=(False, True))
def test_get_community_validations_data(self, mock_exists, mock_open, mock_data):
"""
The main difference between this test and test_get_validations_data
is that this one tries to load first the validations_commons validation
then it fails as os.path.exists returns false and then looks for it in the
community validations.
"""
output = {'Name': 'Advanced Format 512e Support',
'Description': 'foo', 'Groups': ['prep', 'pre-deployment'],
'Categories': ['os', 'storage'],
'Products': ['product1'],
'ID': '512e',
'Parameters': {},
'Path': '/tmp'}
res = utils.get_validations_data('512e')
self.assertEqual(res, output)
@mock.patch('validations_libs.validation.Validation._get_content',
return_value=fakes.FAKE_PLAYBOOK[0])
@mock.patch('six.moves.builtins.open')
@mock.patch('os.path.exists', side_effect=(False, True))
def test_get_community_disabled_validations_data(self, mock_exists, mock_open, mock_data):
"""
This test is similar to test_get_community_validations_data in the sense that it
doesn't find the validations_commons one and should look for community validations
but the setting is disabled by the config so it shouldn't find any validations
"""
output = {}
res = utils.get_validations_data(
'512e',
validation_config={'default': {"enable_community_validations": False}})
self.assertEqual(res, output)
@mock.patch('os.path.exists', return_value=True) @mock.patch('os.path.exists', return_value=True)
def test_get_validations_data_wrong_type(self, mock_exists): def test_get_validations_data_wrong_type(self, mock_exists):
validation = ['val1'] validation = ['val1']
@ -60,11 +98,33 @@ class TestUtils(TestCase):
@mock.patch('glob.glob') @mock.patch('glob.glob')
def test_parse_all_validations_on_disk(self, mock_glob, mock_open, def test_parse_all_validations_on_disk(self, mock_glob, mock_open,
mock_load): mock_load):
mock_glob.return_value = \ mock_glob.side_effect = \
['/foo/playbook/foo.yaml'] (['/foo/playbook/foo.yaml'], [])
result = utils.parse_all_validations_on_disk('/foo/playbook') result = utils.parse_all_validations_on_disk('/foo/playbook')
self.assertEqual(result, [fakes.FAKE_METADATA]) self.assertEqual(result, [fakes.FAKE_METADATA])
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
@mock.patch('glob.glob')
def test_parse_community_validations_on_disk(
self, mock_glob, mock_open, mock_load):
mock_glob.side_effect = \
([], ['/foo/playbook/foo.yaml'])
result = utils.parse_all_validations_on_disk('/foo/playbook')
self.assertEqual(result, [fakes.FAKE_METADATA])
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
@mock.patch('glob.glob')
def test_parse_all_community_disabled_validations_on_disk(
self, mock_glob, mock_open, mock_load):
mock_glob.side_effect = \
([], ['/foo/playbook/foo.yaml'])
result = utils.parse_all_validations_on_disk(
'/foo/playbook',
validation_config={'default': {"enable_community_validations": False}})
self.assertEqual(result, [])
def test_parse_all_validations_on_disk_wrong_path_type(self): def test_parse_all_validations_on_disk_wrong_path_type(self):
self.assertRaises(TypeError, self.assertRaises(TypeError,
utils.parse_all_validations_on_disk, utils.parse_all_validations_on_disk,
@ -118,8 +178,8 @@ class TestUtils(TestCase):
def test_parse_all_validations_on_disk_by_group(self, mock_glob, def test_parse_all_validations_on_disk_by_group(self, mock_glob,
mock_open, mock_open,
mock_load): mock_load):
mock_glob.return_value = \ mock_glob.side_effect = \
['/foo/playbook/foo.yaml'] (['/foo/playbook/foo.yaml'], [])
result = utils.parse_all_validations_on_disk('/foo/playbook', result = utils.parse_all_validations_on_disk('/foo/playbook',
['prep']) ['prep'])
self.assertEqual(result, [fakes.FAKE_METADATA]) self.assertEqual(result, [fakes.FAKE_METADATA])
@ -130,8 +190,8 @@ class TestUtils(TestCase):
def test_parse_all_validations_on_disk_by_category(self, mock_glob, def test_parse_all_validations_on_disk_by_category(self, mock_glob,
mock_open, mock_open,
mock_load): mock_load):
mock_glob.return_value = \ mock_glob.side_effect = \
['/foo/playbook/foo.yaml'] (['/foo/playbook/foo.yaml'], [])
result = utils.parse_all_validations_on_disk('/foo/playbook', result = utils.parse_all_validations_on_disk('/foo/playbook',
categories=['os']) categories=['os'])
self.assertEqual(result, [fakes.FAKE_METADATA]) self.assertEqual(result, [fakes.FAKE_METADATA])
@ -147,31 +207,80 @@ class TestUtils(TestCase):
def test_parse_all_validations_on_disk_by_product(self, mock_glob, def test_parse_all_validations_on_disk_by_product(self, mock_glob,
mock_open, mock_open,
mock_load): mock_load):
mock_glob.return_value = \ mock_glob.side_effect = (['/foo/playbook/foo.yaml'], [])
['/foo/playbook/foo.yaml']
result = utils.parse_all_validations_on_disk('/foo/playbook', result = utils.parse_all_validations_on_disk('/foo/playbook',
products=['product1']) products=['product1'])
self.assertEqual(result, [fakes.FAKE_METADATA]) self.assertEqual(result, [fakes.FAKE_METADATA])
@mock.patch('os.path.isfile') @mock.patch('os.path.isfile')
@mock.patch('os.listdir') @mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open') @mock.patch('six.moves.builtins.open')
def test_get_validations_playbook_by_id(self, mock_open, mock_load, def test_get_validations_playbook_by_id(self, mock_open, mock_load,
mock_listdir, mock_isfile): mock_glob, mock_isfile):
mock_listdir.return_value = ['foo.yaml'] mock_glob.side_effect = (['/foo/playbook/foo.yaml'], [])
mock_isfile.return_value = True mock_isfile.return_value = True
result = utils.get_validations_playbook('/foo/playbook', result = utils.get_validations_playbook('/foo/playbook',
validation_id=['foo']) validation_id=['foo'])
self.assertEqual(result, ['/foo/playbook/foo.yaml']) self.assertEqual(result, ['/foo/playbook/foo.yaml'])
@mock.patch('os.path.isfile') @mock.patch('os.path.isfile')
@mock.patch('os.listdir') @mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_get_community_playbook_by_id(self, mock_open, mock_load,
mock_glob, mock_isfile):
mock_glob.side_effect = (
[],
['/home/foo/community-validations/playbooks/foo.yaml'])
mock_isfile.return_value = True
# AP this needs a bit of an explanation. We look at the explicity at
# the /foo/playbook directory but the community validation path is
# implicit and we find there the id that we are looking for.
result = utils.get_validations_playbook('/foo/playbook',
validation_id=['foo'])
self.assertEqual(result, ['/home/foo/community-validations/playbooks/foo.yaml'])
@mock.patch('os.path.isfile')
@mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_get_community_disabled_playbook_by_id(
self, mock_open, mock_load, mock_glob, mock_isfile):
mock_glob.side_effect = (
[],
['/home/foo/community-validations/playbooks/foo.yaml'])
mock_isfile.return_value = True
# The validations_commons validation is not found and community_vals is disabled
# So no validation should be found.
result = utils.get_validations_playbook(
'/foo/playbook',
validation_id=['foo'],
validation_config={'default': {"enable_community_validations": False}})
self.assertEqual(result, [])
@mock.patch('os.path.isfile')
@mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_get_community_playbook_by_id_not_found(
self, mock_open, mock_load, mock_glob, mock_isfile):
mock_glob.side_effect = (
[],
['/home/foo/community-validations/playbooks/foo.yaml/'])
# the is file fails
mock_isfile.return_value = False
result = utils.get_validations_playbook('/foo/playbook',
validation_id=['foo'])
self.assertEqual(result, [])
@mock.patch('os.path.isfile')
@mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open') @mock.patch('six.moves.builtins.open')
def test_get_validations_playbook_by_id_group(self, mock_open, mock_load, def test_get_validations_playbook_by_id_group(self, mock_open, mock_load,
mock_listdir, mock_isfile): mock_glob, mock_isfile):
mock_listdir.return_value = ['foo.yaml'] mock_glob.side_effect = (['/foo/playbook/foo.yaml'], [])
mock_isfile.return_value = True mock_isfile.return_value = True
result = utils.get_validations_playbook('/foo/playbook', ['foo'], ['prep']) result = utils.get_validations_playbook('/foo/playbook', ['foo'], ['prep'])
self.assertEqual(result, ['/foo/playbook/foo.yaml', self.assertEqual(result, ['/foo/playbook/foo.yaml',
@ -192,24 +301,24 @@ class TestUtils(TestCase):
self.assertEqual(result, []) self.assertEqual(result, [])
@mock.patch('os.path.isfile') @mock.patch('os.path.isfile')
@mock.patch('os.listdir') @mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open') @mock.patch('six.moves.builtins.open')
def test_get_validations_playbook_by_category(self, mock_open, mock_load, def test_get_validations_playbook_by_category(self, mock_open, mock_load,
mock_listdir, mock_isfile): mock_glob, mock_isfile):
mock_listdir.return_value = ['foo.yaml'] mock_glob.side_effect = (['/foo/playbook/foo.yaml'], [])
mock_isfile.return_value = True mock_isfile.return_value = True
result = utils.get_validations_playbook('/foo/playbook', result = utils.get_validations_playbook('/foo/playbook',
categories=['os', 'storage']) categories=['os', 'storage'])
self.assertEqual(result, ['/foo/playbook/foo.yaml']) self.assertEqual(result, ['/foo/playbook/foo.yaml'])
@mock.patch('os.path.isfile') @mock.patch('os.path.isfile')
@mock.patch('os.listdir') @mock.patch('glob.glob')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open') @mock.patch('six.moves.builtins.open')
def test_get_validations_playbook_by_product(self, mock_open, mock_load, def test_get_validations_playbook_by_product(self, mock_open, mock_load,
mock_listdir, mock_isfile): mock_glob, mock_isfile):
mock_listdir.return_value = ['foo.yaml'] mock_glob.side_effect = (['/foo/playbook/foo.yaml'], [])
mock_isfile.return_value = True mock_isfile.return_value = True
result = utils.get_validations_playbook('/foo/playbook', result = utils.get_validations_playbook('/foo/playbook',
products=['product1']) products=['product1'])

View File

@ -15,8 +15,10 @@
try: try:
from unittest import mock from unittest import mock
from unittest.mock import ANY
except ImportError: except ImportError:
import mock import mock
from mock import ANY
import json import json
@ -209,7 +211,8 @@ class TestValidationActions(TestCase):
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'id': 'foo', 'id': 'foo',
'name': 'My Validition One Name', 'name': 'My Validition One Name',
'parameters': {}}] 'parameters': {},
'path': '/tmp/foobar/validation-playbooks'}]
mock_ansible_run.return_value = ('foo.yaml', 0, 'successful') mock_ansible_run.return_value = ('foo.yaml', 0, 'successful')
@ -224,6 +227,29 @@ class TestValidationActions(TestCase):
validations_dir='/tmp/foo') validations_dir='/tmp/foo')
self.assertEqual(run_return, expected_run_return) self.assertEqual(run_return, expected_run_return)
mock_ansible_run.assert_called_with(
workdir=ANY,
playbook='/tmp/foobar/validation-playbooks/foo.yaml',
base_dir='/usr/share/ansible',
playbook_dir='/tmp/foobar/validation-playbooks',
parallel_run=True,
inventory='tmp/inventory.yaml',
output_callback='validation_stdout',
callback_whitelist=None,
quiet=True,
extra_vars=None,
limit_hosts=None,
extra_env_variables=None,
ansible_cfg_file=None,
gathering_policy='explicit',
ansible_artifact_path=ANY,
log_path=ANY,
run_async=False,
python_interpreter=None,
ssh_user=None,
validation_cfg_file=None
)
@mock.patch('validations_libs.utils.get_validations_playbook') @mock.patch('validations_libs.utils.get_validations_playbook')
def test_validation_run_wrong_validation_name(self, mock_validation_play): def test_validation_run_wrong_validation_name(self, mock_validation_play):
mock_validation_play.return_value = [] mock_validation_play.return_value = []
@ -234,6 +260,31 @@ class TestValidationActions(TestCase):
validations_dir='/tmp/foo' validations_dir='/tmp/foo'
) )
@mock.patch('validations_libs.utils.get_validations_playbook')
def test_validation_run_not_all_found(self, mock_validation_play):
mock_validation_play.return_value = ['/tmp/foo/fake.yaml']
run = ValidationActions()
try:
run.run_validations(
validation_name=['fake', 'foo'],
validations_dir='/tmp/foo')
except RuntimeError as runtime_error:
self.assertEqual(
"Validation ['foo'] not found in /tmp/foo.",
str(runtime_error))
else:
self.fail("Runtime error exception should have been raised")
@mock.patch('validations_libs.utils.parse_all_validations_on_disk')
def test_validation_run_not_enough_params(self, mock_validation_play):
mock_validation_play.return_value = []
run = ValidationActions()
self.assertRaises(RuntimeError, run.run_validations,
validations_dir='/tmp/foo'
)
@mock.patch('validations_libs.utils.os.makedirs') @mock.patch('validations_libs.utils.os.makedirs')
@mock.patch('validations_libs.utils.os.access', return_value=True) @mock.patch('validations_libs.utils.os.access', return_value=True)
@mock.patch('validations_libs.utils.os.path.exists', return_value=True) @mock.patch('validations_libs.utils.os.path.exists', return_value=True)
@ -250,7 +301,8 @@ class TestValidationActions(TestCase):
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'id': 'foo', 'id': 'foo',
'name': 'My Validition One Name', 'name': 'My Validition One Name',
'parameters': {}}] 'parameters': {},
'path': '/usr/share/ansible/validation-playbooks'}]
mock_ansible_run.return_value = ('foo.yaml', 0, 'failed') mock_ansible_run.return_value = ('foo.yaml', 0, 'failed')
@ -295,7 +347,8 @@ class TestValidationActions(TestCase):
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'id': 'foo', 'id': 'foo',
'name': 'My Validition One Name', 'name': 'My Validition One Name',
'parameters': {}}] 'parameters': {},
'path': '/usr/share/ansible/validation-playbooks'}]
playbook = ['fake.yaml'] playbook = ['fake.yaml']
inventory = 'tmp/inventory.yaml' inventory = 'tmp/inventory.yaml'
@ -321,7 +374,8 @@ class TestValidationActions(TestCase):
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'id': 'foo', 'id': 'foo',
'name': 'My Validition One Name', 'name': 'My Validition One Name',
'parameters': {}}] 'parameters': {},
'path': '/usr/share/ansible/validation-playbooks'}]
playbook = ['fake.yaml'] playbook = ['fake.yaml']
inventory = 'tmp/inventory.yaml' inventory = 'tmp/inventory.yaml'
@ -357,7 +411,9 @@ class TestValidationActions(TestCase):
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'], 'Products': ['product1'],
'ID': '512e', 'ID': '512e',
'Parameters': {}} 'Parameters': {},
'Path': '/tmp'
}
data.update({'Last execution date': '2019-11-25 13:40:14', data.update({'Last execution date': '2019-11-25 13:40:14',
'Number of execution': 'Total: 1, Passed: 0, Failed: 1'}) 'Number of execution': 'Total: 1, Passed: 0, Failed: 1'})
validations_show = ValidationActions() validations_show = ValidationActions()

View File

@ -37,6 +37,21 @@ def current_time():
return '%sZ' % datetime.datetime.utcnow().isoformat() return '%sZ' % datetime.datetime.utcnow().isoformat()
def community_validations_on(validation_config):
"""Check for flag for community validations to be enabled
The default value is true
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: A boolean with the status of community validations flag
:rtype: `bool`
"""
if not validation_config:
return True
return validation_config.get("default", {}).get("enable_community_validations", True)
def create_log_dir(log_path=constants.VALIDATIONS_LOG_BASEDIR): def create_log_dir(log_path=constants.VALIDATIONS_LOG_BASEDIR):
"""Check for presence of the selected validations log dir. """Check for presence of the selected validations log dir.
Create the directory if needed, and use fallback if that Create the directory if needed, and use fallback if that
@ -136,7 +151,8 @@ def create_artifacts_dir(log_path=constants.VALIDATIONS_LOG_BASEDIR,
def parse_all_validations_on_disk(path, def parse_all_validations_on_disk(path,
groups=None, groups=None,
categories=None, categories=None,
products=None): products=None,
validation_config=None):
"""Return a list of validations metadata which can be sorted by Groups, by """Return a list of validations metadata which can be sorted by Groups, by
Categories or by Products. Categories or by Products.
@ -152,6 +168,10 @@ def parse_all_validations_on_disk(path,
:param products: Products of validations :param products: Products of validations
:type products: `list` :type products: `list`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: A list of validations metadata. :return: A list of validations metadata.
:rtype: `list` :rtype: `list`
@ -192,6 +212,9 @@ def parse_all_validations_on_disk(path,
results = [] results = []
validations_abspath = glob.glob("{path}/*.yaml".format(path=path)) validations_abspath = glob.glob("{path}/*.yaml".format(path=path))
if community_validations_on(validation_config):
validations_abspath.extend(glob.glob("{}/*.yaml".format(
constants.COMMUNITY_PLAYBOOKS_DIR)))
LOG.debug( LOG.debug(
"Attempting to parse validations by:\n" "Attempting to parse validations by:\n"
@ -200,7 +223,6 @@ def parse_all_validations_on_disk(path,
" - products: {}\n" " - products: {}\n"
"from {}".format(groups, categories, products, validations_abspath) "from {}".format(groups, categories, products, validations_abspath)
) )
for playbook in validations_abspath: for playbook in validations_abspath:
val = Validation(playbook) val = Validation(playbook)
@ -220,7 +242,8 @@ def get_validations_playbook(path,
validation_id=None, validation_id=None,
groups=None, groups=None,
categories=None, categories=None,
products=None): products=None,
validation_config=None):
"""Get a list of validations playbooks paths either by their names, """Get a list of validations playbooks paths either by their names,
their groups, by their categories or by their products. their groups, by their categories or by their products.
@ -239,6 +262,10 @@ def get_validations_playbook(path,
:param products: List of validation product :param products: List of validation product
:type products: `list` :type products: `list`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: A list of absolute validations playbooks path :return: A list of absolute validations playbooks path
:rtype: `list` :rtype: `list`
@ -281,12 +308,15 @@ def get_validations_playbook(path,
raise TypeError("The 'products' argument must be a List") raise TypeError("The 'products' argument must be a List")
pl = [] pl = []
for f in os.listdir(path): validations_abspath = glob.glob("{path}/*.yaml".format(path=path))
pl_path = join(path, f) if community_validations_on(validation_config):
validations_abspath.extend(glob.glob("{}/*.yaml".format(
constants.COMMUNITY_PLAYBOOKS_DIR)))
for pl_path in validations_abspath:
if os.path.isfile(pl_path): if os.path.isfile(pl_path):
if validation_id: if validation_id:
if os.path.splitext(f)[0] in validation_id or \ if os.path.splitext(os.path.basename(pl_path))[0] in validation_id or \
os.path.basename(f) in validation_id: os.path.basename(pl_path) in validation_id:
pl.append(pl_path) pl.append(pl_path)
val = Validation(pl_path) val = Validation(pl_path)
@ -377,7 +407,10 @@ def get_validations_details(validation):
return {} return {}
def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR): def get_validations_data(
validation,
path=constants.ANSIBLE_VALIDATION_DIR,
validation_config=None):
"""Return validation data with format: """Return validation data with format:
ID, Name, Description, Groups, Parameters ID, Name, Description, Groups, Parameters
@ -387,6 +420,9 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR):
:type validation: `string` :type validation: `string`
:param path: The path to the validations directory :param path: The path to the validations directory
:type path: `string` :type path: `string`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: The validation data with the format :return: The validation data with the format
(ID, Name, Description, Groups, Parameters) (ID, Name, Description, Groups, Parameters)
:rtype: `dict` :rtype: `dict`
@ -408,6 +444,9 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR):
data = {} data = {}
val_path = "{}/{}.yaml".format(path, validation) val_path = "{}/{}.yaml".format(path, validation)
comm_path = ""
if community_validations_on(validation_config):
comm_path = "{}/{}.yaml".format(constants.COMMUNITY_PLAYBOOKS_DIR, validation)
LOG.debug( LOG.debug(
"Obtaining information about validation {} from {}".format( "Obtaining information about validation {} from {}".format(
@ -419,6 +458,11 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR):
val = Validation(val_path) val = Validation(val_path)
data.update(val.get_formated_data) data.update(val.get_formated_data)
data.update({'Parameters': val.get_vars}) data.update({'Parameters': val.get_vars})
if not data and comm_path:
if os.path.exists(comm_path):
val = Validation(comm_path)
data.update(val.get_formated_data)
data.update({'Parameters': val.get_vars})
return data return data

View File

@ -89,6 +89,7 @@ class Validation(object):
def __init__(self, validation_path): def __init__(self, validation_path):
self.dict = self._get_content(validation_path) self.dict = self._get_content(validation_path)
self.id = os.path.splitext(os.path.basename(validation_path))[0] self.id = os.path.splitext(os.path.basename(validation_path))[0]
self.path = os.path.dirname(validation_path)
def _get_content(self, val_path): def _get_content(self, val_path):
try: try:
@ -176,10 +177,11 @@ class Validation(object):
'categories': ['category1', 'category2'], 'categories': ['category1', 'category2'],
'products': ['product1', 'product2'], 'products': ['product1', 'product2'],
'id': 'val1', 'id': 'val1',
'name': 'The validation val1\'s name'} 'name': 'The validation val1\'s name',
'path': '/tmp/foo/'}
""" """
if self.has_metadata_dict: if self.has_metadata_dict:
self.metadata = {'id': self.id} self.metadata = {'id': self.id, 'path': self.path}
self.metadata.update(self.dict['vars'].get('metadata')) self.metadata.update(self.dict['vars'].get('metadata'))
return self.metadata return self.metadata
else: else:
@ -353,7 +355,8 @@ class Validation(object):
'Description': 'description of val', 'Description': 'description of val',
'Groups': ['group1', 'group2'], 'Groups': ['group1', 'group2'],
'ID': 'val', 'ID': 'val',
'Name': 'validation one'} 'Name': 'validation one',
'path': '/tmp/foo/'}
""" """
data = {} data = {}
metadata = self.get_metadata metadata = self.get_metadata

View File

@ -50,7 +50,8 @@ class ValidationActions(object):
def list_validations(self, def list_validations(self,
groups=None, groups=None,
categories=None, categories=None,
products=None): products=None,
validation_config=None):
"""Get a list of the validations selected by group membership or by """Get a list of the validations selected by group membership or by
category. With their names, group membership information, categories and category. With their names, group membership information, categories and
products. products.
@ -66,6 +67,10 @@ class ValidationActions(object):
:param products: List of validation products. :param products: List of validation products.
:type products: `list` :type products: `list`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: Column names and a list of the selected validations :return: Column names and a list of the selected validations
:rtype: `tuple` :rtype: `tuple`
@ -106,7 +111,8 @@ class ValidationActions(object):
path=self.validation_path, path=self.validation_path,
groups=groups, groups=groups,
categories=categories, categories=categories,
products=products products=products,
validation_config=validation_config
) )
self.log.debug( self.log.debug(
@ -124,13 +130,18 @@ class ValidationActions(object):
return (column_names, return_values) return (column_names, return_values)
def show_validations(self, validation, def show_validations(self, validation,
log_path=constants.VALIDATIONS_LOG_BASEDIR): log_path=constants.VALIDATIONS_LOG_BASEDIR,
validation_config=None):
"""Display detailed information about a Validation """Display detailed information about a Validation
:param validation: The name of the validation :param validation: The name of the validation
:type validation: `string` :type validation: `string`
:param log_path: The absolute path of the validations logs :param log_path: The absolute path of the validations logs
:type log_path: `string` :type log_path: `string`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: The detailed information for a validation :return: The detailed information for a validation
:rtype: `dict` :rtype: `dict`
@ -156,11 +167,18 @@ class ValidationActions(object):
self.log = logging.getLogger(__name__ + ".show_validations") self.log = logging.getLogger(__name__ + ".show_validations")
# Get validation data: # Get validation data:
vlog = ValidationLogs(log_path) vlog = ValidationLogs(log_path)
data = v_utils.get_validations_data(validation, self.validation_path) data = v_utils.get_validations_data(
if not data:
msg = "Validation {} not found in the path: {}".format(
validation, validation,
self.validation_path) self.validation_path,
validation_config=validation_config)
if not data:
extra_msg = ""
if v_utils.community_validations_on(validation_config):
extra_msg = " or {}".format(constants.COMMUNITY_LIBRARY_DIR)
msg = "Validation {} not found in the path: {}{}".format(
validation,
self.validation_path,
extra_msg)
raise RuntimeError(msg) raise RuntimeError(msg)
logfiles = vlog.get_logfile_content_by_validation(validation) logfiles = vlog.get_logfile_content_by_validation(validation)
data_format = vlog.get_validations_stats(logfiles) data_format = vlog.get_validations_stats(logfiles)
@ -331,15 +349,15 @@ class ValidationActions(object):
(Defaults to 'None') (Defaults to 'None')
:type skip_list: ``dict`` :type skip_list: ``dict``
:return: A list of dictionary containing the informations of the
validations executions (Validations, Duration, Host_Group,
Status, Status_by_Host, UUID and Unreachable_Hosts)
:rtype: ``list``
:param ssh_user: Ssh user for Ansible remote connection :param ssh_user: Ssh user for Ansible remote connection
:type ssh_user: ``string`` :type ssh_user: ``string``
:param validation_config: A dictionary of configuration for Validation :param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file. loaded from an validation.cfg file.
:type validation_config: ``dict`` :type validation_config: ``dict``
:return: A list of dictionary containing the informations of the
validations executions (Validations, Duration, Host_Group,
Status, Status_by_Host, UUID and Unreachable_Hosts)
:rtype: ``list``
:Example: :Example:
@ -378,13 +396,16 @@ class ValidationActions(object):
) )
validations = v_utils.parse_all_validations_on_disk( validations = v_utils.parse_all_validations_on_disk(
path=validations_dir, groups=group, path=validations_dir, groups=group,
categories=category, products=product categories=category, products=product,
validation_config=validation_config
) )
for val in validations: for val in validations:
playbooks.append(val.get('id') + '.yaml') playbooks.append("{path}/{id}.yaml".format(**val))
elif validation_name: elif validation_name:
playbooks = v_utils.get_validations_playbook(validations_dir, playbooks = v_utils.get_validations_playbook(
validation_name) validations_dir,
validation_name,
validation_config=validation_config)
if not playbooks or len(validation_name) != len(playbooks): if not playbooks or len(validation_name) != len(playbooks):
p = [] p = []
@ -419,7 +440,7 @@ class ValidationActions(object):
workdir=artifacts_dir, workdir=artifacts_dir,
playbook=playbook, playbook=playbook,
base_dir=base_dir, base_dir=base_dir,
playbook_dir=validations_dir, playbook_dir=os.path.dirname(playbook),
parallel_run=True, parallel_run=True,
inventory=inventory, inventory=inventory,
output_callback=output_callback, output_callback=output_callback,
@ -441,7 +462,7 @@ class ValidationActions(object):
workdir=artifacts_dir, workdir=artifacts_dir,
playbook=playbook, playbook=playbook,
base_dir=base_dir, base_dir=base_dir,
playbook_dir=validations_dir, playbook_dir=os.path.dirname(playbook),
parallel_run=True, parallel_run=True,
inventory=inventory, inventory=inventory,
output_callback=output_callback, output_callback=output_callback,
@ -483,7 +504,7 @@ class ValidationActions(object):
vlog = ValidationLogs(log_path) vlog = ValidationLogs(log_path)
return vlog.get_results(uuid) return vlog.get_results(uuid)
def group_information(self, groups): def group_information(self, groups, validation_config=None):
"""Get Information about Validation Groups """Get Information about Validation Groups
This is used to print table from python ``Tuple`` with ``PrettyTable``. This is used to print table from python ``Tuple`` with ``PrettyTable``.
@ -500,6 +521,9 @@ class ValidationActions(object):
:param groups: The absolute path of the groups.yaml file :param groups: The absolute path of the groups.yaml file
:type groups: ``string`` :type groups: ``string``
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: The list of the available groups with their description and :return: The list of the available groups with their description and
the numbers of validation belonging to them. the numbers of validation belonging to them.
@ -523,7 +547,8 @@ class ValidationActions(object):
validations = v_utils.parse_all_validations_on_disk( validations = v_utils.parse_all_validations_on_disk(
path=self.validation_path, path=self.validation_path,
groups=[group[0] for group in group_definitions]) groups=[group[0] for group in group_definitions],
validation_config=validation_config)
# Get validations number by group # Get validations number by group
for group in group_definitions: for group in group_definitions:
@ -543,7 +568,8 @@ class ValidationActions(object):
categories=None, categories=None,
products=None, products=None,
output_format='json', output_format='json',
download_file=None): download_file=None,
validation_config=None):
""" """
Return Validations Parameters for one or several validations by their Return Validations Parameters for one or several validations by their
names, their groups, by their categories or by their products. names, their groups, by their categories or by their products.
@ -566,6 +592,9 @@ class ValidationActions(object):
:param download_file: Path of a file in which the parameters will be :param download_file: Path of a file in which the parameters will be
stored stored
:type download_file: `string` :type download_file: `string`
:param validation_config: A dictionary of configuration for Validation
loaded from an validation.cfg file.
:type validation_config: ``dict``
:return: A JSON or a YAML dump (By default, JSON). :return: A JSON or a YAML dump (By default, JSON).
if `download_file` is used, a file containing only the if `download_file` is used, a file containing only the
@ -622,7 +651,8 @@ class ValidationActions(object):
validation_id=validations, validation_id=validations,
groups=groups, groups=groups,
categories=categories, categories=categories,
products=products products=products,
validation_config=validation_config
) )
params = v_utils.get_validations_parameters( params = v_utils.get_validations_parameters(