Add Products metadata key management

This patch adds the management of the new `products` metadata key in the
validation playbooks. We can now filter the validations by their groups,
by their categories or by their products while listing or running them.

The `list` sub command has now a new --product argument. When filtering
by groups, by categories or by products (see the example below), the
`list` sub command will return all the validation playbooks belonging to
the 'prep' group OR all the validation playbooks belonging to the 'os'
and/or 'system' categories OR all the validation playbooks to the
'tripleo' product:

$ validation list -h
$ validation list --group prep --category os,system --product tripleo

The `run` sub command has also its new --product argument. Note that
this new argument is mutually exclusive with the --validation, --group
and --category arguments:

$ validation run -h
$ validation run --product tripleo --inventory /etc/ansible/hosts

The `show parameter` sub command has the same new argument which is also
mutually exclusive with the --validation, --group and the --category
arguments:

$ validation show parameter -h
$ validation show parameter --product tripleo

Change-Id: Ief13e506c2bee18da47b31f1c2d5c0dbb1ad1ecf
Signed-off-by: Gael Chamoulaud (Strider) <gchamoul@redhat.com>
This commit is contained in:
Gael Chamoulaud (Strider) 2021-07-12 13:58:04 +02:00 committed by Gael Chamoulaud
parent 3928305329
commit c46c90394c
13 changed files with 316 additions and 55 deletions

View File

@ -43,6 +43,13 @@ class ValidationList(Lister):
help=("List specific category of validations, " help=("List specific category of validations, "
"if more than one category is required " "if more than one category is required "
"separate the category names with commas.")) "separate the category names with commas."))
parser.add_argument('--product',
metavar='<product_id>[,<product_id>,...]',
action=CommaListAction,
default=[],
help=("List specific product of validations, "
"if more than one product is required "
"separate the product names with commas."))
parser.add_argument('--validation-dir', dest='validation_dir', parser.add_argument('--validation-dir', dest='validation_dir',
default=constants.ANSIBLE_VALIDATION_DIR, default=constants.ANSIBLE_VALIDATION_DIR,
help=("Path where the validation playbooks " help=("Path where the validation playbooks "
@ -54,8 +61,10 @@ class ValidationList(Lister):
group = parsed_args.group group = parsed_args.group
category = parsed_args.category category = parsed_args.category
product = parsed_args.product
validation_dir = parsed_args.validation_dir validation_dir = parsed_args.validation_dir
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))

View File

@ -148,6 +148,15 @@ class Run(BaseCommand):
"if more than one category is required " "if more than one category is required "
"separate the category names with commas.")) "separate the category names with commas."))
ex_group.add_argument(
'--product',
metavar='<product_id>[,<product_id>,...]',
action=CommaListAction,
default=[],
help=("Run specific validations by product, "
"if more than one product is required "
"separate the product names with commas."))
return parser return parser
def take_action(self, parsed_args): def take_action(self, parsed_args):
@ -178,6 +187,7 @@ class Run(BaseCommand):
limit_hosts=parsed_args.limit, limit_hosts=parsed_args.limit,
group=parsed_args.group, group=parsed_args.group,
category=parsed_args.category, category=parsed_args.category,
product=parsed_args.product,
extra_vars=extra_vars, extra_vars=extra_vars,
validations_dir=parsed_args.validation_dir, validations_dir=parsed_args.validation_dir,
base_dir=parsed_args.ansible_base_dir, base_dir=parsed_args.ansible_base_dir,

View File

@ -118,6 +118,15 @@ class ShowParameter(ShowOne):
"if more than one category is required " "if more than one category is required "
"separate the category names with commas.")) "separate the category names with commas."))
ex_group.add_argument(
'--product',
metavar='<product_id>[,<product_id>,...]',
action=CommaListAction,
default=[],
help=("List specific validations by product, "
"if more than one product is required "
"separate the product names with commas."))
parser.add_argument( parser.add_argument(
'--download', '--download',
action='store', action='store',
@ -146,6 +155,7 @@ class ShowParameter(ShowOne):
validations=parsed_args.validation_name, validations=parsed_args.validation_name,
groups=parsed_args.group, groups=parsed_args.group,
categories=parsed_args.category, categories=parsed_args.category,
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)

View File

@ -40,6 +40,7 @@ class TestList(BaseCommand):
{'description': 'My Validation One Description', {'description': 'My Validation One Description',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'system', 'ram'], 'categories': ['os', 'system', 'ram'],
'products': ['product1'],
'id': 'my_val1', 'id': 'my_val1',
'name': 'My Validation One Name', 'name': 'My Validation One Name',
'parameters': {} 'parameters': {}
@ -47,6 +48,7 @@ class TestList(BaseCommand):
'description': 'My Validation Two Description', 'description': 'My Validation Two Description',
'groups': ['prep', 'pre-introspection'], 'groups': ['prep', 'pre-introspection'],
'categories': ['networking'], 'categories': ['networking'],
'products': ['product1'],
'id': 'my_val2', 'id': 'my_val2',
'name': 'My Validation Two Name', 'name': 'My Validation Two Name',
'parameters': {'min_value': 8} 'parameters': {'min_value': 8}
@ -92,3 +94,16 @@ class TestList(BaseCommand):
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args) result = self.cmd.take_action(parsed_args)
self.assertEqual(result, val_list) self.assertEqual(result, val_list)
@mock.patch('validations_libs.utils.parse_all_validations_on_disk',
return_value=fakes.VALIDATIONS_LIST_GROUP)
def test_list_validations_by_product(self, mock_list):
arglist = ['--validation-dir', 'foo', '--product', 'product1']
verifylist = [('validation_dir', 'foo'),
('product', ['product1'])]
val_list = fakes.VALIDATION_LIST_RESULT
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
result = self.cmd.take_action(parsed_args)
self.assertEqual(result, val_list)

View File

@ -72,6 +72,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': {'key': 'value'}, 'extra_vars': {'key': 'value'},
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -105,6 +106,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': {'key': 'value2'}, 'extra_vars': {'key': 'value2'},
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -151,6 +153,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': {'key': 'value'}, 'extra_vars': {'key': 'value'},
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -182,6 +185,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': None, 'extra_vars': None,
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -218,6 +222,7 @@ class TestRun(BaseCommand):
'quiet': False, 'quiet': False,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': None, 'extra_vars': None,
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -248,6 +253,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': None, 'extra_vars': None,
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -283,6 +289,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': {'key': 'value'}, 'extra_vars': {'key': 'value'},
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -325,6 +332,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': None, 'extra_vars': None,
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',
@ -352,6 +360,7 @@ class TestRun(BaseCommand):
'limit_hosts': None, 'limit_hosts': None,
'group': [], 'group': [],
'category': [], 'category': [],
'product': [],
'extra_vars': {'key': 'value'}, 'extra_vars': {'key': 'value'},
'validations_dir': '/usr/share/ansible/validation-playbooks', 'validations_dir': '/usr/share/ansible/validation-playbooks',
'base_dir': '/usr/share/ansible', 'base_dir': '/usr/share/ansible',

View File

@ -98,3 +98,11 @@ class TestShowParameter(BaseCommand):
verifylist = [('category', ['os'])] verifylist = [('category', ['os'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args) self.cmd.take_action(parsed_args)
@mock.patch('validations_libs.validation_actions.ValidationActions.'
'show_validations_parameters')
def test_show_validations_parameters_by_products(self, mock_show):
arglist = ['--product', 'product1']
verifylist = [('product', ['product1'])]
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
self.cmd.take_action(parsed_args)

View File

@ -19,6 +19,7 @@ VALIDATIONS_LIST = [{
'description': 'My Validation One Description', 'description': 'My Validation One Description',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'system', 'ram'], 'categories': ['os', 'system', 'ram'],
'products': ['product1'],
'id': 'my_val1', 'id': 'my_val1',
'name': 'My Validation One Name', 'name': 'My Validation One Name',
'parameters': {} 'parameters': {}
@ -26,6 +27,7 @@ VALIDATIONS_LIST = [{
'description': 'My Validation Two Description', 'description': 'My Validation Two Description',
'groups': ['prep', 'pre-introspection'], 'groups': ['prep', 'pre-introspection'],
'categories': ['networking'], 'categories': ['networking'],
'products': ['product1'],
'id': 'my_val2', 'id': 'my_val2',
'name': 'My Validation Two Name', 'name': 'My Validation Two Name',
'parameters': {'min_value': 8} 'parameters': {'min_value': 8}
@ -35,16 +37,18 @@ VALIDATIONS_LIST_GROUP = [{
'description': 'My Validation Two Description', 'description': 'My Validation Two Description',
'groups': ['prep', 'pre-introspection'], 'groups': ['prep', 'pre-introspection'],
'categories': ['networking'], 'categories': ['networking'],
'products': ['product1'],
'id': 'my_val2', 'id': 'my_val2',
'name': 'My Validation Two Name', 'name': 'My Validation Two Name',
'parameters': {'min_value': 8} 'parameters': {'min_value': 8}
}] }]
VALIDATION_LIST_RESULT = (('ID', 'Name', 'Groups', 'Categories'), VALIDATION_LIST_RESULT = (('ID', 'Name', 'Groups', 'Categories', 'Products'),
[('my_val2', 'My Validation Two Name', [('my_val2', 'My Validation Two Name',
['prep', 'pre-introspection'], ['prep', 'pre-introspection'],
['networking'])]) ['networking'],
['product1'])])
GROUPS_LIST = [ GROUPS_LIST = [
('group1', 'Group1 description'), ('group1', 'Group1 description'),
@ -214,6 +218,7 @@ VALIDATIONS_LOGS_CONTENTS_LIST = [{
VALIDATIONS_DATA = {'Description': 'My Validation One Description', VALIDATIONS_DATA = {'Description': 'My Validation One Description',
'Groups': ['prep', 'pre-deployment'], 'Groups': ['prep', 'pre-deployment'],
'categories': ['os', 'system', 'ram'], 'categories': ['os', 'system', 'ram'],
'products': ['product1'],
'ID': 'my_val1', 'ID': 'my_val1',
'Name': 'My Validation One Name', 'Name': 'My Validation One Name',
'parameters': {}} 'parameters': {}}
@ -229,6 +234,7 @@ FAKE_WRONG_PLAYBOOK = [{
'description': 'foo', 'description': 'foo',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'],
'name': 'Advanced Format 512e Support' 'name': 'Advanced Format 512e Support'
} }
} }
@ -239,6 +245,7 @@ FAKE_PLAYBOOK = [{'hosts': 'undercloud',
'vars': {'metadata': {'description': 'foo', 'vars': {'metadata': {'description': 'foo',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'],
'name': 'name':
'Advanced Format 512e Support'}}}] 'Advanced Format 512e Support'}}}]
@ -247,6 +254,7 @@ FAKE_PLAYBOOK2 = [{'hosts': 'undercloud',
'vars': {'metadata': {'description': 'foo', 'vars': {'metadata': {'description': 'foo',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'],
'name': 'name':
'Advanced Format 512e Support'}, 'Advanced Format 512e Support'},
'foo': 'bar'}}] 'foo': 'bar'}}]
@ -264,11 +272,13 @@ FAKE_METADATA = {'id': 'foo',
'description': 'foo', 'description': 'foo',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'categories': ['os', 'storage'], 'categories': ['os', 'storage'],
'products': ['product1'],
'name': 'Advanced Format 512e Support'} 'name': 'Advanced Format 512e Support'}
FORMATED_DATA = {'Description': 'foo', FORMATED_DATA = {'Description': 'foo',
'Groups': ['prep', 'pre-deployment'], 'Groups': ['prep', 'pre-deployment'],
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'],
'ID': 'foo', 'ID': 'foo',
'Name': 'Advanced Format 512e Support'} 'Name': 'Advanced Format 512e Support'}

View File

@ -37,6 +37,7 @@ class TestUtils(TestCase):
output = {'Name': 'Advanced Format 512e Support', output = {'Name': 'Advanced Format 512e Support',
'Description': 'foo', 'Groups': ['prep', 'pre-deployment'], 'Description': 'foo', 'Groups': ['prep', 'pre-deployment'],
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'],
'ID': '512e', 'ID': '512e',
'Parameters': {}} 'Parameters': {}}
res = utils.get_validations_data('512e') res = utils.get_validations_data('512e')
@ -76,6 +77,12 @@ class TestUtils(TestCase):
path='/foo/playbook', path='/foo/playbook',
categories='foo1,foo2') categories='foo1,foo2')
def test_parse_all_validations_on_disk_wrong_products_type(self):
self.assertRaises(TypeError,
utils.parse_all_validations_on_disk,
path='/foo/playbook',
products='foo1,foo2')
def test_get_validations_playbook_wrong_validation_id_type(self): def test_get_validations_playbook_wrong_validation_id_type(self):
self.assertRaises(TypeError, self.assertRaises(TypeError,
utils.get_validations_playbook, utils.get_validations_playbook,
@ -94,6 +101,12 @@ class TestUtils(TestCase):
path='/foo/playbook', path='/foo/playbook',
categories='foo1,foo2') categories='foo1,foo2')
def test_get_validations_playbook_wrong_products_type(self):
self.assertRaises(TypeError,
utils.get_validations_playbook,
path='/foo/playbook',
products='foo1,foo2')
@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')
@mock.patch('glob.glob') @mock.patch('glob.glob')
@ -123,6 +136,18 @@ class TestUtils(TestCase):
utils.get_validations_playbook, utils.get_validations_playbook,
path=['/foo/playbook']) path=['/foo/playbook'])
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
@mock.patch('glob.glob')
def test_parse_all_validations_on_disk_by_product(self, mock_glob,
mock_open,
mock_load):
mock_glob.return_value = \
['/foo/playbook/foo.yaml']
result = utils.parse_all_validations_on_disk('/foo/playbook',
products=['product1'])
self.assertEqual(result, [fakes.FAKE_METADATA])
@mock.patch('os.path.isfile') @mock.patch('os.path.isfile')
@mock.patch('os.listdir') @mock.patch('os.listdir')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@ -173,6 +198,18 @@ class TestUtils(TestCase):
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.listdir')
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_get_validations_playbook_by_product(self, mock_open, mock_load,
mock_listdir, mock_isfile):
mock_listdir.return_value = ['foo.yaml']
mock_isfile.return_value = True
result = utils.get_validations_playbook('/foo/playbook',
products=['product1'])
self.assertEqual(result, ['/foo/playbook/foo.yaml'])
@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_validation_parameters(self, mock_open, mock_load): def test_get_validation_parameters(self, mock_open, mock_load):

View File

@ -125,6 +125,28 @@ class TestValidation(TestCase):
categories = val.categories categories = val.categories
self.assertEqual(categories, []) self.assertEqual(categories, [])
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_products(self, mock_open, mock_yaml):
val = Validation('/tmp/foo')
products = val.products
self.assertEqual(products, ['product1'])
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_WRONG_PLAYBOOK)
@mock.patch('six.moves.builtins.open')
def test_products_with_no_metadata(self, mock_open, mock_yaml):
with self.assertRaises(NameError) as exc_mgr:
Validation('/tmp/foo').products
self.assertEqual('No metadata found in validation foo',
str(exc_mgr.exception))
@mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK3)
@mock.patch('six.moves.builtins.open')
def test_products_with_no_existing_products(self, mock_open, mock_yaml):
val = Validation('/tmp/foo')
products = val.products
self.assertEqual(products, [])
@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_ordered_dict(self, mock_open, mock_yaml): def test_get_ordered_dict(self, mock_open, mock_yaml):

View File

@ -30,7 +30,7 @@ class TestValidationActions(TestCase):
def setUp(self): def setUp(self):
super(TestValidationActions, self).setUp() super(TestValidationActions, self).setUp()
self.column_name = ('ID', 'Name', 'Groups', 'Categories') self.column_name = ('ID', 'Name', 'Groups', 'Categories', 'Products')
@mock.patch('validations_libs.utils.parse_all_validations_on_disk', @mock.patch('validations_libs.utils.parse_all_validations_on_disk',
return_value=fakes.VALIDATIONS_LIST) return_value=fakes.VALIDATIONS_LIST)
@ -41,11 +41,13 @@ class TestValidationActions(TestCase):
(self.column_name, [('my_val1', (self.column_name, [('my_val1',
'My Validation One Name', 'My Validation One Name',
['prep', 'pre-deployment'], ['prep', 'pre-deployment'],
['os', 'system', 'ram']), ['os', 'system', 'ram'],
['product1']),
('my_val2', ('my_val2',
'My Validation Two Name', 'My Validation Two Name',
['prep', 'pre-introspection'], ['prep', 'pre-introspection'],
['networking'])])) ['networking'],
['product1'])]))
@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)
@ -351,6 +353,7 @@ class TestValidationActions(TestCase):
data = {'Name': 'Advanced Format 512e Support', data = {'Name': 'Advanced Format 512e Support',
'Description': 'foo', 'Groups': ['prep', 'pre-deployment'], 'Description': 'foo', 'Groups': ['prep', 'pre-deployment'],
'Categories': ['os', 'storage'], 'Categories': ['os', 'storage'],
'Products': ['product1'],
'ID': '512e', 'ID': '512e',
'Parameters': {}} 'Parameters': {}}
data.update({'Last execution date': '2019-11-25 13:40:14', data.update({'Last execution date': '2019-11-25 13:40:14',
@ -402,6 +405,13 @@ class TestValidationActions(TestCase):
v_actions.show_validations_parameters, v_actions.show_validations_parameters,
categories={'foo': 'bar'}) categories={'foo': 'bar'})
@mock.patch('six.moves.builtins.open')
def test_show_validations_parameters_wrong_products_type(self, mock_open):
v_actions = ValidationActions()
self.assertRaises(TypeError,
v_actions.show_validations_parameters,
products={'foo': 'bar'})
@mock.patch('validations_libs.utils.get_validations_playbook', @mock.patch('validations_libs.utils.get_validations_playbook',
return_value=['/foo/playbook/foo.yaml']) return_value=['/foo/playbook/foo.yaml'])
@mock.patch('validations_libs.utils.get_validations_parameters') @mock.patch('validations_libs.utils.get_validations_parameters')

View File

@ -128,9 +128,12 @@ def create_artifacts_dir(log_path=constants.VALIDATIONS_LOG_BASEDIR,
raise RuntimeError() raise RuntimeError()
def parse_all_validations_on_disk(path, groups=None, categories=None): def parse_all_validations_on_disk(path,
"""Return a list of validations metadata which can be sorted by Groups or by groups=None,
Categories. categories=None,
products=None):
"""Return a list of validations metadata which can be sorted by Groups, by
Categories or by Products.
:param path: The absolute path of the validations directory :param path: The absolute path of the validations directory
:type path: `string` :type path: `string`
@ -141,6 +144,9 @@ def parse_all_validations_on_disk(path, groups=None, categories=None):
:param categories: Categories of validations :param categories: Categories of validations
:type categories: `list` :type categories: `list`
:param products: Products of validations
:type products: `list`
:return: A list of validations metadata. :return: A list of validations metadata.
:rtype: `list` :rtype: `list`
@ -149,11 +155,13 @@ def parse_all_validations_on_disk(path, groups=None, categories=None):
>>> path = '/foo/bar' >>> path = '/foo/bar'
>>> parse_all_validations_on_disk(path) >>> parse_all_validations_on_disk(path)
[{'categories': ['storage'], [{'categories': ['storage'],
'products': ['product1'],
'description': 'Detect whether the node disks use Advanced Format.', 'description': 'Detect whether the node disks use Advanced Format.',
'groups': ['prep', 'pre-deployment'], 'groups': ['prep', 'pre-deployment'],
'id': '512e', 'id': '512e',
'name': 'Advanced Format 512e Support'}, 'name': 'Advanced Format 512e Support'},
{'categories': ['system'], {'categories': ['system'],
'products': ['product1'],
'description': 'Make sure that the server has enough CPU cores.', 'description': 'Make sure that the server has enough CPU cores.',
'groups': ['prep', 'pre-introspection'], 'groups': ['prep', 'pre-introspection'],
'id': 'check-cpu', 'id': 'check-cpu',
@ -172,6 +180,11 @@ def parse_all_validations_on_disk(path, groups=None, categories=None):
elif not isinstance(categories, list): elif not isinstance(categories, list):
raise TypeError("The 'categories' argument must be a List") raise TypeError("The 'categories' argument must be a List")
if not products:
products = []
elif not isinstance(products, list):
raise TypeError("The 'products' argument must be a List")
results = [] results = []
validations_abspath = glob.glob("{path}/*.yaml".format(path=path)) validations_abspath = glob.glob("{path}/*.yaml".format(path=path))
@ -179,18 +192,20 @@ def parse_all_validations_on_disk(path, groups=None, categories=None):
"Attempting to parse validations by:\n" "Attempting to parse validations by:\n"
" - groups: {}\n" " - groups: {}\n"
" - categories: {}\n" " - categories: {}\n"
"from {}".format(groups, categories, validations_abspath) " - products: {}\n"
"from {}".format(groups, categories, products, validations_abspath)
) )
for playbook in validations_abspath: for playbook in validations_abspath:
val = Validation(playbook) val = Validation(playbook)
if not groups and not categories: if not groups and not categories and not products:
results.append(val.get_metadata) results.append(val.get_metadata)
continue continue
if set(groups).intersection(val.groups) or \ if set(groups).intersection(val.groups) or \
set(categories).intersection(val.categories): set(categories).intersection(val.categories) or \
set(products).intersection(val.products):
results.append(val.get_metadata) results.append(val.get_metadata)
return results return results
@ -199,9 +214,10 @@ def parse_all_validations_on_disk(path, groups=None, categories=None):
def get_validations_playbook(path, def get_validations_playbook(path,
validation_id=None, validation_id=None,
groups=None, groups=None,
categories=None): categories=None,
products=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 or by their categories. their groups, by their categories or by their products.
:param path: Path of the validations playbooks :param path: Path of the validations playbooks
:type path: `string` :type path: `string`
@ -215,6 +231,9 @@ def get_validations_playbook(path,
:param categories: List of validation category :param categories: List of validation category
:type categories: `list` :type categories: `list`
:param products: List of validation product
:type products: `list`
:return: A list of absolute validations playbooks path :return: A list of absolute validations playbooks path
:rtype: `list` :rtype: `list`
@ -224,7 +243,12 @@ def get_validations_playbook(path,
>>> validation_id = ['512e','check-cpu'] >>> validation_id = ['512e','check-cpu']
>>> groups = None >>> groups = None
>>> categories = None >>> categories = None
>>> get_validations_playbook(path, validation_id, groups, categories) >>> products = None
>>> get_validations_playbook(path=path,
validation_id=validation_id,
groups=groups,
categories=categories,
products=products)
['/usr/share/ansible/validation-playbooks/512e.yaml', ['/usr/share/ansible/validation-playbooks/512e.yaml',
'/usr/share/ansible/validation-playbooks/check-cpu.yaml',] '/usr/share/ansible/validation-playbooks/check-cpu.yaml',]
""" """
@ -246,6 +270,11 @@ def get_validations_playbook(path,
elif not isinstance(categories, list): elif not isinstance(categories, list):
raise TypeError("The 'categories' argument must be a List") raise TypeError("The 'categories' argument must be a List")
if not products:
products = []
elif not isinstance(products, list):
raise TypeError("The 'products' argument must be a List")
pl = [] pl = []
for f in os.listdir(path): for f in os.listdir(path):
pl_path = join(path, f) pl_path = join(path, f)
@ -262,6 +291,9 @@ def get_validations_playbook(path,
if categories: if categories:
if set(categories).intersection(val.categories): if set(categories).intersection(val.categories):
pl.append(pl_path) pl.append(pl_path)
if products:
if set(products).intersection(val.products):
pl.append(pl_path)
return pl return pl
@ -326,6 +358,7 @@ def get_validations_details(validation):
{'description': 'Verify that the server has enough something.', {'description': 'Verify that the server has enough something.',
'groups': ['group1', 'group2'], 'groups': ['group1', 'group2'],
'categories': ['category1', 'category2'], 'categories': ['category1', 'category2'],
'products': ['product1', 'product2'],
'id': 'check-something', 'id': 'check-something',
'name': 'Verify the server fits the something requirements'} 'name': 'Verify the server fits the something requirements'}
""" """
@ -360,6 +393,7 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR):
{'Description': 'Verify that the server has enough something', {'Description': 'Verify that the server has enough something',
'Groups': ['group1', 'group2'], 'Groups': ['group1', 'group2'],
'Categories': ['category1', 'category2'], 'Categories': ['category1', 'category2'],
'products': ['product1', 'product2'],
'ID': 'check-something', 'ID': 'check-something',
'Name': 'Verify the server fits the something requirements', 'Name': 'Verify the server fits the something requirements',
'Parameters': {'param1': 24}} 'Parameters': {'param1': 24}}
@ -386,18 +420,26 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR):
def get_validations_parameters(validations_data, def get_validations_parameters(validations_data,
validation_name=None, validation_name=None,
groups=None, groups=None,
categories=None): categories=None,
products=None):
"""Return parameters for a list of validations """Return parameters for a list of validations
:param validations_data: A list of absolute validations playbooks path :param validations_data: A list of absolute validations playbooks path
:type validations_data: `list` :type validations_data: `list`
:param validation_name: A list of validation name :param validation_name: A list of validation name
:type validation_name: `list` :type validation_name: `list`
:param groups: A list of validation groups :param groups: A list of validation groups
:type groups: `list` :type groups: `list`
:param categories: A list of validation categories :param categories: A list of validation categories
:type categories: `list` :type categories: `list`
:param products: A list of validation products
:type products: `list`
:return: a dictionary containing the current parameters for :return: a dictionary containing the current parameters for
each `validation_name` or `groups` each `validation_name` or `groups`
:rtype: `dict` :rtype: `dict`
@ -429,12 +471,18 @@ def get_validations_parameters(validations_data,
elif not isinstance(categories, list): elif not isinstance(categories, list):
raise TypeError("The 'categories' argument must be a List") raise TypeError("The 'categories' argument must be a List")
if not products:
products = []
elif not isinstance(products, list):
raise TypeError("The 'products' argument must be a List")
params = {} params = {}
for val in validations_data: for val in validations_data:
v = Validation(val) v = Validation(val)
if v.id in validation_name or \ if v.id in validation_name or \
set(groups).intersection(v.groups) or \ set(groups).intersection(v.groups) or \
set(categories).intersection(v.categories): set(categories).intersection(v.categories) or \
set(products).intersection(v.products):
params[v.id] = { params[v.id] = {
'parameters': v.get_vars 'parameters': v.get_vars
} }

View File

@ -49,13 +49,14 @@ class Validation(object):
``metadata`` section to read validation's name and description. These ``metadata`` section to read validation's name and description. These
values are then reported by the API. values are then reported by the API.
The validations can be grouped together by specifying a ``groups`` The validations can be grouped together by specifying a ``groups``, a
and a ``categories`` metadata. ``groups`` are the deployment stage the ``categories`` and a ``products`` metadata. ``groups`` are the deployment
validations should run on and ``categories`` are the technical stage the validations should run on, ``categories`` are the technical
classification for the validations. classification for the validations and ``products`` are the specific
validations which should be executed against a specific product.
Groups and Categories function similar to tags and a validation can thus be Groups, Categories and Products function similar to tags and a validation
part of many groups and many categories. can thus be part of many groups and many categories.
Here is an example: Here is an example:
@ -74,12 +75,16 @@ class Validation(object):
- networking - networking
- storage - storage
- security - security
products:
- product1
- product2
roles: roles:
- hello_world - hello_world
""" """
_col_keys = ['ID', 'Name', 'Description', 'Groups', 'Categories'] _col_keys = ['ID', 'Name', 'Description',
'Groups', 'Categories', 'Products']
def __init__(self, validation_path): def __init__(self, validation_path):
self.dict = self._get_content(validation_path) self.dict = self._get_content(validation_path)
@ -111,6 +116,9 @@ class Validation(object):
- networking - networking
- storage - storage
- security - security
products:
- product1
- product2
roles: roles:
- hello_world - hello_world
@ -138,6 +146,9 @@ class Validation(object):
- networking - networking
- storage - storage
- security - security
products:
- product1
- product2
roles: roles:
- hello_world - hello_world
@ -163,6 +174,7 @@ class Validation(object):
{'description': 'Val1 desc.', {'description': 'Val1 desc.',
'groups': ['group1', 'group2'], 'groups': ['group1', 'group2'],
'categories': ['category1', 'category2'], 'categories': ['category1', 'category2'],
'products': ['product1', 'product2'],
'id': 'val1', 'id': 'val1',
'name': 'The validation val1\'s name'} 'name': 'The validation val1\'s name'}
""" """
@ -219,6 +231,7 @@ class Validation(object):
'vars': {'metadata': {'description': 'description of val ', 'vars': {'metadata': {'description': 'description of val ',
'groups': ['group1', 'group2'], 'groups': ['group1', 'group2'],
'categories': ['category1', 'category2'], 'categories': ['category1', 'category2'],
'products': ['product1', 'product2'],
'name': 'validation one'}, 'name': 'validation one'},
'var_name1': 'value1'}} 'var_name1': 'value1'}}
""" """
@ -270,6 +283,29 @@ class Validation(object):
"No metadata found in validation {}".format(self.id) "No metadata found in validation {}".format(self.id)
) )
@property
def products(self):
"""Get the validation list of products
:return: A list of products for the validation
:rtype: `list` or `None` if no metadata has been found
:raise: A `NameError` exception if no metadata has been found in the
playbook
:Example:
>>> pl = '/foo/bar/val.yaml'
>>> val = Validation(pl)
>>> print(val.products)
['product1', 'product2']
"""
if self.has_metadata_dict:
return self.dict['vars']['metadata'].get('products', [])
else:
raise NameError(
"No metadata found in validation {}".format(self.id)
)
@property @property
def get_id(self): def get_id(self):
"""Get the validation id """Get the validation id
@ -313,6 +349,7 @@ class Validation(object):
>>> val = Validation(pl) >>> val = Validation(pl)
>>> print(val.get_formated_data) >>> print(val.get_formated_data)
{'Categories': ['category1', 'category2'], {'Categories': ['category1', 'category2'],
'Products': ['product1', 'product2'],
'Description': 'description of val', 'Description': 'description of val',
'Groups': ['group1', 'group2'], 'Groups': ['group1', 'group2'],
'ID': 'val', 'ID': 'val',

View File

@ -47,9 +47,13 @@ class ValidationActions(object):
self.validation_path = (validation_path if validation_path self.validation_path = (validation_path if validation_path
else constants.ANSIBLE_VALIDATION_DIR) else constants.ANSIBLE_VALIDATION_DIR)
def list_validations(self, groups=None, categories=None): def list_validations(self,
groups=None,
categories=None,
products=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 and categories. category. With their names, group membership information, categories and
products.
This is used to print table from python ``Tuple`` with ``PrettyTable``. This is used to print table from python ``Tuple`` with ``PrettyTable``.
@ -59,18 +63,21 @@ class ValidationActions(object):
:param categories: List of validation categories. :param categories: List of validation categories.
:type categories: `list` :type categories: `list`
:param products: List of validation products.
:type products: `list`
:return: Column names and a list of the selected validations :return: Column names and a list of the selected validations
:rtype: `tuple` :rtype: `tuple`
.. code:: text .. code:: text
-------+-----------+----------------------+---------------+ -------+-----------+----------------------+---------------+--------------+
| ID | Name | Groups | Categories | | ID | Name | Groups | Categories | Products |
+------+-----------+----------------------+---------------+ +------+-----------+----------------------+---------------+--------------+
| val1 | val_name1 | ['group1'] | ['category1'] | | val1 | val_name1 | ['group1'] | ['category1'] | ['product1'] |
| val2 | val_name2 | ['group1', 'group2'] | ['category2'] | | val2 | val_name2 | ['group1', 'group2'] | ['category2'] | ['product2'] |
| val3 | val_name3 | ['group4'] | ['category3'] | | val3 | val_name3 | ['group4'] | ['category3'] | ['product3'] |
+------+-----------+----------------------+---------------+ +------+-----------+----------------------+---------------+--------------+
:Example: :Example:
@ -81,16 +88,26 @@ class ValidationActions(object):
>>> results = action.list_validations(groups=groups, >>> results = action.list_validations(groups=groups,
categories=categories) categories=categories)
>>> print(results >>> print(results
(('ID', 'Name', 'Groups', 'Categories'), (('ID', 'Name', 'Groups', 'Categories', 'Products'),
[('val1', 'val_name1', ['group1'], ['category1']), [('val1',
('val2', 'val_name2', ['group1', 'group2'], ['category2'])]) 'val_name1',
['group1'],
['category1'],
['product1']),
('val2',
'val_name2',
['group1', 'group2'],
['category2'],
['product2'])])
""" """
self.log = logging.getLogger(__name__ + ".list_validations") self.log = logging.getLogger(__name__ + ".list_validations")
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=groups, groups=groups,
categories=categories) categories=categories,
products=products
)
self.log.debug( self.log.debug(
"Parsed {} validations.".format(len(validations)) "Parsed {} validations.".format(len(validations))
@ -98,10 +115,11 @@ class ValidationActions(object):
return_values = [ return_values = [
(val.get('id'), val.get('name'), (val.get('id'), val.get('name'),
val.get('groups'), val.get('categories')) val.get('groups'), val.get('categories'),
val.get('products'))
for val in validations] for val in validations]
column_names = ('ID', 'Name', 'Groups', 'Categories') column_names = ('ID', 'Name', 'Groups', 'Categories', 'Products')
return (column_names, return_values) return (column_names, return_values)
@ -241,17 +259,17 @@ class ValidationActions(object):
return [path[1] for path in logs[-history_limit:]] return [path[1] for path in logs[-history_limit:]]
def run_validations(self, validation_name=None, inventory='localhost', def run_validations(self, validation_name=None, inventory='localhost',
group=None, category=None, extra_vars=None, group=None, category=None, product=None,
validations_dir=None, extra_env_vars=None, extra_vars=None, validations_dir=None,
ansible_cfg=None, quiet=True, workdir=None, extra_env_vars=None, ansible_cfg=None, quiet=True,
limit_hosts=None, run_async=False, workdir=None, limit_hosts=None, run_async=False,
base_dir=constants.DEFAULT_VALIDATIONS_BASEDIR, base_dir=constants.DEFAULT_VALIDATIONS_BASEDIR,
log_path=constants.VALIDATIONS_LOG_BASEDIR, log_path=constants.VALIDATIONS_LOG_BASEDIR,
python_interpreter=None, skip_list=None, python_interpreter=None, skip_list=None,
callback_whitelist=None, callback_whitelist=None,
output_callback='validation_stdout', output_callback='validation_stdout', ssh_user=None):
ssh_user=None): """Run one or multiple validations by name(s), by group(s) or by
"""Run one or multiple validations by name(s) or by group(s) product(s)
:param validation_name: A list of validation names :param validation_name: A list of validation names
:type validation_name: ``list`` :type validation_name: ``list``
@ -262,6 +280,8 @@ class ValidationActions(object):
:type group: ``list`` :type group: ``list``
:param category: A list of category names :param category: A list of category names
:type category: ``list`` :type category: ``list``
:param product: A list of product names
:type product: ``list``
:param extra_vars: Set additional variables as a Dict or the absolute :param extra_vars: Set additional variables as a Dict or the absolute
path of a JSON or YAML file type. path of a JSON or YAML file type.
:type extra_vars: Either a Dict or the absolute path of JSON or YAML :type extra_vars: Either a Dict or the absolute path of JSON or YAML
@ -345,20 +365,22 @@ class ValidationActions(object):
playbooks = [] playbooks = []
validations_dir = (validations_dir if validations_dir validations_dir = (validations_dir if validations_dir
else self.validation_path) else self.validation_path)
if group or category: if group or category or product:
self.log.debug( self.log.debug(
"Getting the validations list by:\n" "Getting the validations list by:\n"
" - groups: {}\n" " - groups: {}\n"
" - categories: {}".format(group, category) " - categories: {}\n"
" - products: {}".format(group, category, product)
) )
validations = v_utils.parse_all_validations_on_disk( validations = v_utils.parse_all_validations_on_disk(
path=validations_dir, groups=group, categories=category) path=validations_dir, groups=group,
categories=category, products=product
)
for val in validations: for val in validations:
playbooks.append(val.get('id') + '.yaml') playbooks.append(val.get('id') + '.yaml')
elif validation_name: elif validation_name:
playbooks = v_utils.get_validations_playbook(validations_dir, playbooks = v_utils.get_validations_playbook(validations_dir,
validation_name, validation_name)
group)
if not playbooks or len(validation_name) != len(playbooks): if not playbooks or len(validation_name) != len(playbooks):
p = [] p = []
@ -495,11 +517,12 @@ class ValidationActions(object):
validations=None, validations=None,
groups=None, groups=None,
categories=None, categories=None,
products=None,
output_format='json', output_format='json',
download_file=None): download_file=None):
""" """
Return Validations Parameters for one or several validations by their Return Validations Parameters for one or several validations by their
names, their groups or by their categories. names, their groups, by their categories or by their products.
:param validations: List of validation name(s) :param validations: List of validation name(s)
:type validations: `list` :type validations: `list`
@ -510,6 +533,9 @@ class ValidationActions(object):
:param categories: List of validation category(ies) :param categories: List of validation category(ies)
:type categories: `list` :type categories: `list`
:param products: List of validation product(s)
:type products: `list`
:param output_format: Output format (Supported format are JSON or YAML) :param output_format: Output format (Supported format are JSON or YAML)
:type output_format: `string` :type output_format: `string`
@ -525,9 +551,10 @@ class ValidationActions(object):
>>> validations = ['check-cpu', 'check-ram'] >>> validations = ['check-cpu', 'check-ram']
>>> groups = None >>> groups = None
>>> categories = None >>> categories = None
>>> products = None
>>> output_format = 'json' >>> output_format = 'json'
>>> show_validations_parameters(validations, groups, >>> show_validations_parameters(validations, groups,
categories, output_format) categories, products, output_format)
{ {
"check-cpu": { "check-cpu": {
"parameters": { "parameters": {
@ -556,6 +583,11 @@ class ValidationActions(object):
elif not isinstance(categories, list): elif not isinstance(categories, list):
raise TypeError("The 'categories' argument must be a List") raise TypeError("The 'categories' argument must be a List")
if not products:
products = []
elif not isinstance(products, list):
raise TypeError("The 'products' argument must be a List")
supported_format = ['json', 'yaml'] supported_format = ['json', 'yaml']
if output_format not in supported_format: if output_format not in supported_format:
@ -565,13 +597,17 @@ class ValidationActions(object):
path=self.validation_path, path=self.validation_path,
validation_id=validations, validation_id=validations,
groups=groups, groups=groups,
categories=categories) categories=categories,
products=products
)
params = v_utils.get_validations_parameters( params = v_utils.get_validations_parameters(
validations_data=validation_playbooks, validations_data=validation_playbooks,
validation_name=validations, validation_name=validations,
groups=groups, groups=groups,
categories=categories) categories=categories,
products=products
)
if download_file: if download_file:
params_only = {} params_only = {}