From c46c90394c22e38ded951744f0c514b410b10353 Mon Sep 17 00:00:00 2001 From: "Gael Chamoulaud (Strider)" Date: Mon, 12 Jul 2021 13:58:04 +0200 Subject: [PATCH] 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) --- validations_libs/cli/lister.py | 11 ++- validations_libs/cli/run.py | 10 ++ validations_libs/cli/show.py | 10 ++ validations_libs/tests/cli/test_list.py | 15 +++ validations_libs/tests/cli/test_run.py | 9 ++ validations_libs/tests/cli/test_show.py | 8 ++ validations_libs/tests/fakes.py | 14 ++- validations_libs/tests/test_utils.py | 37 +++++++ validations_libs/tests/test_validation.py | 22 +++++ .../tests/test_validation_actions.py | 16 ++- validations_libs/utils.py | 70 ++++++++++--- validations_libs/validation.py | 51 ++++++++-- validations_libs/validation_actions.py | 98 +++++++++++++------ 13 files changed, 316 insertions(+), 55 deletions(-) diff --git a/validations_libs/cli/lister.py b/validations_libs/cli/lister.py index 18336eb7..61408436 100644 --- a/validations_libs/cli/lister.py +++ b/validations_libs/cli/lister.py @@ -43,6 +43,13 @@ class ValidationList(Lister): help=("List specific category of validations, " "if more than one category is required " "separate the category names with commas.")) + parser.add_argument('--product', + metavar='[,,...]', + 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', default=constants.ANSIBLE_VALIDATION_DIR, help=("Path where the validation playbooks " @@ -54,8 +61,10 @@ class ValidationList(Lister): group = parsed_args.group category = parsed_args.category + product = parsed_args.product validation_dir = parsed_args.validation_dir v_actions = ValidationActions(validation_path=validation_dir) return (v_actions.list_validations(groups=group, - categories=category)) + categories=category, + products=product)) diff --git a/validations_libs/cli/run.py b/validations_libs/cli/run.py index 6070c0bd..9661df98 100644 --- a/validations_libs/cli/run.py +++ b/validations_libs/cli/run.py @@ -148,6 +148,15 @@ class Run(BaseCommand): "if more than one category is required " "separate the category names with commas.")) + ex_group.add_argument( + '--product', + metavar='[,,...]', + action=CommaListAction, + default=[], + help=("Run specific validations by product, " + "if more than one product is required " + "separate the product names with commas.")) + return parser def take_action(self, parsed_args): @@ -178,6 +187,7 @@ class Run(BaseCommand): limit_hosts=parsed_args.limit, group=parsed_args.group, category=parsed_args.category, + product=parsed_args.product, extra_vars=extra_vars, validations_dir=parsed_args.validation_dir, base_dir=parsed_args.ansible_base_dir, diff --git a/validations_libs/cli/show.py b/validations_libs/cli/show.py index 6b8a8f3d..029f2799 100644 --- a/validations_libs/cli/show.py +++ b/validations_libs/cli/show.py @@ -118,6 +118,15 @@ class ShowParameter(ShowOne): "if more than one category is required " "separate the category names with commas.")) + ex_group.add_argument( + '--product', + metavar='[,,...]', + 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( '--download', action='store', @@ -146,6 +155,7 @@ class ShowParameter(ShowOne): validations=parsed_args.validation_name, groups=parsed_args.group, categories=parsed_args.category, + products=parsed_args.product, output_format=parsed_args.format_output, download_file=parsed_args.download) diff --git a/validations_libs/tests/cli/test_list.py b/validations_libs/tests/cli/test_list.py index eb34cf19..1a5ad41c 100644 --- a/validations_libs/tests/cli/test_list.py +++ b/validations_libs/tests/cli/test_list.py @@ -40,6 +40,7 @@ class TestList(BaseCommand): {'description': 'My Validation One Description', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'system', 'ram'], + 'products': ['product1'], 'id': 'my_val1', 'name': 'My Validation One Name', 'parameters': {} @@ -47,6 +48,7 @@ class TestList(BaseCommand): 'description': 'My Validation Two Description', 'groups': ['prep', 'pre-introspection'], 'categories': ['networking'], + 'products': ['product1'], 'id': 'my_val2', 'name': 'My Validation Two Name', 'parameters': {'min_value': 8} @@ -92,3 +94,16 @@ class TestList(BaseCommand): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) 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) diff --git a/validations_libs/tests/cli/test_run.py b/validations_libs/tests/cli/test_run.py index b95bb86d..c4bca8c3 100644 --- a/validations_libs/tests/cli/test_run.py +++ b/validations_libs/tests/cli/test_run.py @@ -72,6 +72,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': {'key': 'value'}, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -105,6 +106,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': {'key': 'value2'}, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -151,6 +153,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': {'key': 'value'}, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -182,6 +185,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': None, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -218,6 +222,7 @@ class TestRun(BaseCommand): 'quiet': False, 'group': [], 'category': [], + 'product': [], 'extra_vars': None, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -248,6 +253,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': None, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -283,6 +289,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': {'key': 'value'}, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -325,6 +332,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': None, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', @@ -352,6 +360,7 @@ class TestRun(BaseCommand): 'limit_hosts': None, 'group': [], 'category': [], + 'product': [], 'extra_vars': {'key': 'value'}, 'validations_dir': '/usr/share/ansible/validation-playbooks', 'base_dir': '/usr/share/ansible', diff --git a/validations_libs/tests/cli/test_show.py b/validations_libs/tests/cli/test_show.py index 55695012..b9915fa6 100644 --- a/validations_libs/tests/cli/test_show.py +++ b/validations_libs/tests/cli/test_show.py @@ -98,3 +98,11 @@ class TestShowParameter(BaseCommand): verifylist = [('category', ['os'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) 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) diff --git a/validations_libs/tests/fakes.py b/validations_libs/tests/fakes.py index b42ac448..ac93d4a7 100644 --- a/validations_libs/tests/fakes.py +++ b/validations_libs/tests/fakes.py @@ -19,6 +19,7 @@ VALIDATIONS_LIST = [{ 'description': 'My Validation One Description', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'system', 'ram'], + 'products': ['product1'], 'id': 'my_val1', 'name': 'My Validation One Name', 'parameters': {} @@ -26,6 +27,7 @@ VALIDATIONS_LIST = [{ 'description': 'My Validation Two Description', 'groups': ['prep', 'pre-introspection'], 'categories': ['networking'], + 'products': ['product1'], 'id': 'my_val2', 'name': 'My Validation Two Name', 'parameters': {'min_value': 8} @@ -35,16 +37,18 @@ VALIDATIONS_LIST_GROUP = [{ 'description': 'My Validation Two Description', 'groups': ['prep', 'pre-introspection'], 'categories': ['networking'], + 'products': ['product1'], 'id': 'my_val2', 'name': 'My Validation Two Name', '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', ['prep', 'pre-introspection'], - ['networking'])]) + ['networking'], + ['product1'])]) GROUPS_LIST = [ ('group1', 'Group1 description'), @@ -214,6 +218,7 @@ VALIDATIONS_LOGS_CONTENTS_LIST = [{ VALIDATIONS_DATA = {'Description': 'My Validation One Description', 'Groups': ['prep', 'pre-deployment'], 'categories': ['os', 'system', 'ram'], + 'products': ['product1'], 'ID': 'my_val1', 'Name': 'My Validation One Name', 'parameters': {}} @@ -229,6 +234,7 @@ FAKE_WRONG_PLAYBOOK = [{ 'description': 'foo', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'storage'], + 'products': ['product1'], 'name': 'Advanced Format 512e Support' } } @@ -239,6 +245,7 @@ FAKE_PLAYBOOK = [{'hosts': 'undercloud', 'vars': {'metadata': {'description': 'foo', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'storage'], + 'products': ['product1'], 'name': 'Advanced Format 512e Support'}}}] @@ -247,6 +254,7 @@ FAKE_PLAYBOOK2 = [{'hosts': 'undercloud', 'vars': {'metadata': {'description': 'foo', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'storage'], + 'products': ['product1'], 'name': 'Advanced Format 512e Support'}, 'foo': 'bar'}}] @@ -264,11 +272,13 @@ FAKE_METADATA = {'id': 'foo', 'description': 'foo', 'groups': ['prep', 'pre-deployment'], 'categories': ['os', 'storage'], + 'products': ['product1'], 'name': 'Advanced Format 512e Support'} FORMATED_DATA = {'Description': 'foo', 'Groups': ['prep', 'pre-deployment'], 'Categories': ['os', 'storage'], + 'Products': ['product1'], 'ID': 'foo', 'Name': 'Advanced Format 512e Support'} diff --git a/validations_libs/tests/test_utils.py b/validations_libs/tests/test_utils.py index caf56b19..c68f353e 100644 --- a/validations_libs/tests/test_utils.py +++ b/validations_libs/tests/test_utils.py @@ -37,6 +37,7 @@ class TestUtils(TestCase): output = {'Name': 'Advanced Format 512e Support', 'Description': 'foo', 'Groups': ['prep', 'pre-deployment'], 'Categories': ['os', 'storage'], + 'Products': ['product1'], 'ID': '512e', 'Parameters': {}} res = utils.get_validations_data('512e') @@ -76,6 +77,12 @@ class TestUtils(TestCase): path='/foo/playbook', 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): self.assertRaises(TypeError, utils.get_validations_playbook, @@ -94,6 +101,12 @@ class TestUtils(TestCase): path='/foo/playbook', 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('six.moves.builtins.open') @mock.patch('glob.glob') @@ -123,6 +136,18 @@ class TestUtils(TestCase): utils.get_validations_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.listdir') @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @@ -173,6 +198,18 @@ class TestUtils(TestCase): categories=['os', 'storage']) 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('six.moves.builtins.open') def test_get_validation_parameters(self, mock_open, mock_load): diff --git a/validations_libs/tests/test_validation.py b/validations_libs/tests/test_validation.py index de42de3c..3702b8aa 100644 --- a/validations_libs/tests/test_validation.py +++ b/validations_libs/tests/test_validation.py @@ -125,6 +125,28 @@ class TestValidation(TestCase): categories = val.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('six.moves.builtins.open') def test_get_ordered_dict(self, mock_open, mock_yaml): diff --git a/validations_libs/tests/test_validation_actions.py b/validations_libs/tests/test_validation_actions.py index b554937e..d96ebf5f 100644 --- a/validations_libs/tests/test_validation_actions.py +++ b/validations_libs/tests/test_validation_actions.py @@ -30,7 +30,7 @@ class TestValidationActions(TestCase): def setUp(self): 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', return_value=fakes.VALIDATIONS_LIST) @@ -41,11 +41,13 @@ class TestValidationActions(TestCase): (self.column_name, [('my_val1', 'My Validation One Name', ['prep', 'pre-deployment'], - ['os', 'system', 'ram']), + ['os', 'system', 'ram'], + ['product1']), ('my_val2', 'My Validation Two Name', ['prep', 'pre-introspection'], - ['networking'])])) + ['networking'], + ['product1'])])) @mock.patch('validations_libs.utils.os.access', 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', 'Description': 'foo', 'Groups': ['prep', 'pre-deployment'], 'Categories': ['os', 'storage'], + 'Products': ['product1'], 'ID': '512e', 'Parameters': {}} data.update({'Last execution date': '2019-11-25 13:40:14', @@ -402,6 +405,13 @@ class TestValidationActions(TestCase): v_actions.show_validations_parameters, 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', return_value=['/foo/playbook/foo.yaml']) @mock.patch('validations_libs.utils.get_validations_parameters') diff --git a/validations_libs/utils.py b/validations_libs/utils.py index 01abcd09..ae9a1caf 100644 --- a/validations_libs/utils.py +++ b/validations_libs/utils.py @@ -128,9 +128,12 @@ def create_artifacts_dir(log_path=constants.VALIDATIONS_LOG_BASEDIR, raise RuntimeError() -def parse_all_validations_on_disk(path, groups=None, categories=None): - """Return a list of validations metadata which can be sorted by Groups or by - Categories. +def parse_all_validations_on_disk(path, + groups=None, + 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 :type path: `string` @@ -141,6 +144,9 @@ def parse_all_validations_on_disk(path, groups=None, categories=None): :param categories: Categories of validations :type categories: `list` + :param products: Products of validations + :type products: `list` + :return: A list of validations metadata. :rtype: `list` @@ -149,11 +155,13 @@ def parse_all_validations_on_disk(path, groups=None, categories=None): >>> path = '/foo/bar' >>> parse_all_validations_on_disk(path) [{'categories': ['storage'], + 'products': ['product1'], 'description': 'Detect whether the node disks use Advanced Format.', 'groups': ['prep', 'pre-deployment'], 'id': '512e', 'name': 'Advanced Format 512e Support'}, {'categories': ['system'], + 'products': ['product1'], 'description': 'Make sure that the server has enough CPU cores.', 'groups': ['prep', 'pre-introspection'], 'id': 'check-cpu', @@ -172,6 +180,11 @@ def parse_all_validations_on_disk(path, groups=None, categories=None): elif not isinstance(categories, 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 = [] 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" " - groups: {}\n" " - categories: {}\n" - "from {}".format(groups, categories, validations_abspath) + " - products: {}\n" + "from {}".format(groups, categories, products, validations_abspath) ) for playbook in validations_abspath: val = Validation(playbook) - if not groups and not categories: + if not groups and not categories and not products: results.append(val.get_metadata) continue 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) return results @@ -199,9 +214,10 @@ def parse_all_validations_on_disk(path, groups=None, categories=None): def get_validations_playbook(path, validation_id=None, groups=None, - categories=None): + categories=None, + products=None): """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 :type path: `string` @@ -215,6 +231,9 @@ def get_validations_playbook(path, :param categories: List of validation category :type categories: `list` + :param products: List of validation product + :type products: `list` + :return: A list of absolute validations playbooks path :rtype: `list` @@ -224,7 +243,12 @@ def get_validations_playbook(path, >>> validation_id = ['512e','check-cpu'] >>> groups = 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/check-cpu.yaml',] """ @@ -246,6 +270,11 @@ def get_validations_playbook(path, elif not isinstance(categories, 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 = [] for f in os.listdir(path): pl_path = join(path, f) @@ -262,6 +291,9 @@ def get_validations_playbook(path, if categories: if set(categories).intersection(val.categories): pl.append(pl_path) + if products: + if set(products).intersection(val.products): + pl.append(pl_path) return pl @@ -326,6 +358,7 @@ def get_validations_details(validation): {'description': 'Verify that the server has enough something.', 'groups': ['group1', 'group2'], 'categories': ['category1', 'category2'], + 'products': ['product1', 'product2'], 'id': 'check-something', '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', 'Groups': ['group1', 'group2'], 'Categories': ['category1', 'category2'], + 'products': ['product1', 'product2'], 'ID': 'check-something', 'Name': 'Verify the server fits the something requirements', 'Parameters': {'param1': 24}} @@ -386,18 +420,26 @@ def get_validations_data(validation, path=constants.ANSIBLE_VALIDATION_DIR): def get_validations_parameters(validations_data, validation_name=None, groups=None, - categories=None): + categories=None, + products=None): """Return parameters for a list of validations :param validations_data: A list of absolute validations playbooks path :type validations_data: `list` + :param validation_name: A list of validation name :type validation_name: `list` + :param groups: A list of validation groups :type groups: `list` + :param categories: A list of validation categories :type categories: `list` + + :param products: A list of validation products + :type products: `list` + :return: a dictionary containing the current parameters for each `validation_name` or `groups` :rtype: `dict` @@ -429,12 +471,18 @@ def get_validations_parameters(validations_data, elif not isinstance(categories, 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 = {} for val in validations_data: v = Validation(val) if v.id in validation_name 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] = { 'parameters': v.get_vars } diff --git a/validations_libs/validation.py b/validations_libs/validation.py index 1c99d549..330e288f 100644 --- a/validations_libs/validation.py +++ b/validations_libs/validation.py @@ -49,13 +49,14 @@ class Validation(object): ``metadata`` section to read validation's name and description. These values are then reported by the API. - The validations can be grouped together by specifying a ``groups`` - and a ``categories`` metadata. ``groups`` are the deployment stage the - validations should run on and ``categories`` are the technical - classification for the validations. + The validations can be grouped together by specifying a ``groups``, a + ``categories`` and a ``products`` metadata. ``groups`` are the deployment + stage the validations should run on, ``categories`` are the technical + 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 - part of many groups and many categories. + Groups, Categories and Products function similar to tags and a validation + can thus be part of many groups and many categories. Here is an example: @@ -74,12 +75,16 @@ class Validation(object): - networking - storage - security + products: + - product1 + - product2 roles: - hello_world """ - _col_keys = ['ID', 'Name', 'Description', 'Groups', 'Categories'] + _col_keys = ['ID', 'Name', 'Description', + 'Groups', 'Categories', 'Products'] def __init__(self, validation_path): self.dict = self._get_content(validation_path) @@ -111,6 +116,9 @@ class Validation(object): - networking - storage - security + products: + - product1 + - product2 roles: - hello_world @@ -138,6 +146,9 @@ class Validation(object): - networking - storage - security + products: + - product1 + - product2 roles: - hello_world @@ -163,6 +174,7 @@ class Validation(object): {'description': 'Val1 desc.', 'groups': ['group1', 'group2'], 'categories': ['category1', 'category2'], + 'products': ['product1', 'product2'], 'id': 'val1', 'name': 'The validation val1\'s name'} """ @@ -219,6 +231,7 @@ class Validation(object): 'vars': {'metadata': {'description': 'description of val ', 'groups': ['group1', 'group2'], 'categories': ['category1', 'category2'], + 'products': ['product1', 'product2'], 'name': 'validation one'}, 'var_name1': 'value1'}} """ @@ -270,6 +283,29 @@ class Validation(object): "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 def get_id(self): """Get the validation id @@ -313,6 +349,7 @@ class Validation(object): >>> val = Validation(pl) >>> print(val.get_formated_data) {'Categories': ['category1', 'category2'], + 'Products': ['product1', 'product2'], 'Description': 'description of val', 'Groups': ['group1', 'group2'], 'ID': 'val', diff --git a/validations_libs/validation_actions.py b/validations_libs/validation_actions.py index b021b536..21b8354f 100644 --- a/validations_libs/validation_actions.py +++ b/validations_libs/validation_actions.py @@ -47,9 +47,13 @@ class ValidationActions(object): self.validation_path = (validation_path if validation_path 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 - 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``. @@ -59,18 +63,21 @@ class ValidationActions(object): :param categories: List of validation categories. :type categories: `list` + :param products: List of validation products. + :type products: `list` + :return: Column names and a list of the selected validations :rtype: `tuple` .. code:: text - -------+-----------+----------------------+---------------+ - | ID | Name | Groups | Categories | - +------+-----------+----------------------+---------------+ - | val1 | val_name1 | ['group1'] | ['category1'] | - | val2 | val_name2 | ['group1', 'group2'] | ['category2'] | - | val3 | val_name3 | ['group4'] | ['category3'] | - +------+-----------+----------------------+---------------+ + -------+-----------+----------------------+---------------+--------------+ + | ID | Name | Groups | Categories | Products | + +------+-----------+----------------------+---------------+--------------+ + | val1 | val_name1 | ['group1'] | ['category1'] | ['product1'] | + | val2 | val_name2 | ['group1', 'group2'] | ['category2'] | ['product2'] | + | val3 | val_name3 | ['group4'] | ['category3'] | ['product3'] | + +------+-----------+----------------------+---------------+--------------+ :Example: @@ -81,16 +88,26 @@ class ValidationActions(object): >>> results = action.list_validations(groups=groups, categories=categories) >>> print(results - (('ID', 'Name', 'Groups', 'Categories'), - [('val1', 'val_name1', ['group1'], ['category1']), - ('val2', 'val_name2', ['group1', 'group2'], ['category2'])]) + (('ID', 'Name', 'Groups', 'Categories', 'Products'), + [('val1', + 'val_name1', + ['group1'], + ['category1'], + ['product1']), + ('val2', + 'val_name2', + ['group1', 'group2'], + ['category2'], + ['product2'])]) """ self.log = logging.getLogger(__name__ + ".list_validations") validations = v_utils.parse_all_validations_on_disk( path=self.validation_path, groups=groups, - categories=categories) + categories=categories, + products=products + ) self.log.debug( "Parsed {} validations.".format(len(validations)) @@ -98,10 +115,11 @@ class ValidationActions(object): return_values = [ (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] - column_names = ('ID', 'Name', 'Groups', 'Categories') + column_names = ('ID', 'Name', 'Groups', 'Categories', 'Products') return (column_names, return_values) @@ -241,17 +259,17 @@ class ValidationActions(object): return [path[1] for path in logs[-history_limit:]] def run_validations(self, validation_name=None, inventory='localhost', - group=None, category=None, extra_vars=None, - validations_dir=None, extra_env_vars=None, - ansible_cfg=None, quiet=True, workdir=None, - limit_hosts=None, run_async=False, + group=None, category=None, product=None, + extra_vars=None, validations_dir=None, + extra_env_vars=None, ansible_cfg=None, quiet=True, + workdir=None, limit_hosts=None, run_async=False, base_dir=constants.DEFAULT_VALIDATIONS_BASEDIR, log_path=constants.VALIDATIONS_LOG_BASEDIR, python_interpreter=None, skip_list=None, callback_whitelist=None, - output_callback='validation_stdout', - ssh_user=None): - """Run one or multiple validations by name(s) or by group(s) + output_callback='validation_stdout', ssh_user=None): + """Run one or multiple validations by name(s), by group(s) or by + product(s) :param validation_name: A list of validation names :type validation_name: ``list`` @@ -262,6 +280,8 @@ class ValidationActions(object): :type group: ``list`` :param category: A list of category names :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 path of a JSON or YAML file type. :type extra_vars: Either a Dict or the absolute path of JSON or YAML @@ -345,20 +365,22 @@ class ValidationActions(object): playbooks = [] validations_dir = (validations_dir if validations_dir else self.validation_path) - if group or category: + if group or category or product: self.log.debug( "Getting the validations list by:\n" " - groups: {}\n" - " - categories: {}".format(group, category) + " - categories: {}\n" + " - products: {}".format(group, category, product) ) 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: playbooks.append(val.get('id') + '.yaml') elif validation_name: playbooks = v_utils.get_validations_playbook(validations_dir, - validation_name, - group) + validation_name) if not playbooks or len(validation_name) != len(playbooks): p = [] @@ -495,11 +517,12 @@ class ValidationActions(object): validations=None, groups=None, categories=None, + products=None, output_format='json', download_file=None): """ 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) :type validations: `list` @@ -510,6 +533,9 @@ class ValidationActions(object): :param categories: List of validation category(ies) :type categories: `list` + :param products: List of validation product(s) + :type products: `list` + :param output_format: Output format (Supported format are JSON or YAML) :type output_format: `string` @@ -525,9 +551,10 @@ class ValidationActions(object): >>> validations = ['check-cpu', 'check-ram'] >>> groups = None >>> categories = None + >>> products = None >>> output_format = 'json' >>> show_validations_parameters(validations, groups, - categories, output_format) + categories, products, output_format) { "check-cpu": { "parameters": { @@ -556,6 +583,11 @@ class ValidationActions(object): elif not isinstance(categories, 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'] if output_format not in supported_format: @@ -565,13 +597,17 @@ class ValidationActions(object): path=self.validation_path, validation_id=validations, groups=groups, - categories=categories) + categories=categories, + products=products + ) params = v_utils.get_validations_parameters( validations_data=validation_playbooks, validation_name=validations, groups=groups, - categories=categories) + categories=categories, + products=products + ) if download_file: params_only = {}