From eb62054a336853c3ea641d781a8b577abf407fca Mon Sep 17 00:00:00 2001 From: "Gael Chamoulaud (Strider)" Date: Tue, 10 Nov 2020 14:42:20 +0100 Subject: [PATCH] Enforce the way we encapsulate a Validation This patch ensures that the playbook is respecting the minimal structure of a validation. This patch also adds DocStrings to the Validation class and new unittests according to the enforcement changes. Change-Id: I0809163cbd661cbd24705ed348281c7818a944b4 Signed-off-by: Gael Chamoulaud (Strider) --- validations_libs/tests/fakes.py | 20 ++ validations_libs/tests/test_validation.py | 53 +++++ validations_libs/validation.py | 239 ++++++++++++++++++++-- 3 files changed, 297 insertions(+), 15 deletions(-) diff --git a/validations_libs/tests/fakes.py b/validations_libs/tests/fakes.py index bc836eb8..9d5541f5 100644 --- a/validations_libs/tests/fakes.py +++ b/validations_libs/tests/fakes.py @@ -174,6 +174,18 @@ VALIDATIONS_DATA = {'Description': 'My Validation One Description', VALIDATIONS_STATS = {'Last execution date': '2019-11-25 13:40:14', 'Number of execution': 'Total: 1, Passed: 0, Failed: 1'} +FAKE_WRONG_PLAYBOOK = [{ + 'hosts': 'undercloud', + 'roles': ['advanced_format_512e_support'], + 'vars': { + 'nometadata': { + 'description': 'foo', + 'groups': ['prep', 'pre-deployment'], + 'name': 'Advanced Format 512e Support' + } + } +}] + FAKE_PLAYBOOK = [{'hosts': 'undercloud', 'roles': ['advanced_format_512e_support'], 'vars': {'metadata': {'description': 'foo', @@ -189,6 +201,14 @@ FAKE_PLAYBOOK2 = [{'hosts': 'undercloud', 'Advanced Format 512e Support'}, 'foo': 'bar'}}] +FAKE_PLAYBOOK3 = [{'hosts': 'undercloud', + 'roles': ['advanced_format_512e_support'], + 'vars': {'metadata': {'description': 'foo', + 'name': + 'Advanced Format 512e Support'}, + 'foo': 'bar'}}] + +FAKE_VARS = {'foo': 'bar'} FAKE_METADATA = {'id': 'foo', 'description': 'foo', diff --git a/validations_libs/tests/test_validation.py b/validations_libs/tests/test_validation.py index b38fc5a3..b337ab92 100644 --- a/validations_libs/tests/test_validation.py +++ b/validations_libs/tests/test_validation.py @@ -42,6 +42,36 @@ class TestValidation(TestCase): data = val.get_metadata self.assertEquals(data, fakes.FAKE_METADATA) + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_WRONG_PLAYBOOK) + @mock.patch('six.moves.builtins.open') + def test_get_metadata_wrong_playbook(self, mock_open, mock_yaml): + with self.assertRaises(NameError) as exc_mgr: + Validation('/tmp/foo').get_metadata + self.assertEqual('No metadata found in validation foo', + str(exc_mgr.exception)) + + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK2) + @mock.patch('six.moves.builtins.open') + def test_get_vars(self, mock_open, mock_yaml): + val = Validation('/tmp/foo') + data = val.get_vars + self.assertEquals(data, fakes.FAKE_VARS) + + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) + @mock.patch('six.moves.builtins.open') + def test_get_vars_no_vars(self, mock_open, mock_yaml): + val = Validation('/tmp/foo') + data = val.get_vars + self.assertEquals(data, {}) + + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_WRONG_PLAYBOOK) + @mock.patch('six.moves.builtins.open') + def test_get_vars_no_metadata(self, mock_open, mock_yaml): + with self.assertRaises(NameError) as exc_mgr: + Validation('/tmp/foo').get_vars + self.assertEqual('No metadata found in validation foo', + str(exc_mgr.exception)) + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_PLAYBOOK) @mock.patch('six.moves.builtins.open') def test_get_id(self, mock_open, mock_yaml): @@ -58,6 +88,21 @@ class TestValidation(TestCase): groups = val.groups self.assertEquals(groups, ['prep', 'pre-deployment']) + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_WRONG_PLAYBOOK) + @mock.patch('six.moves.builtins.open') + def test_groups_with_no_metadata(self, mock_open, mock_yaml): + with self.assertRaises(NameError) as exc_mgr: + Validation('/tmp/foo').groups + 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_groups_with_no_existing_groups(self, mock_open, mock_yaml): + val = Validation('/tmp/foo') + groups = val.groups + self.assertEquals(groups, []) + @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): @@ -72,6 +117,14 @@ class TestValidation(TestCase): data = val.get_formated_data self.assertEquals(data, fakes.FORMATED_DATA) + @mock.patch('yaml.safe_load', return_value=fakes.FAKE_WRONG_PLAYBOOK) + @mock.patch('six.moves.builtins.open') + def test_get_formated_data_no_metadata(self, mock_open, mock_yaml): + with self.assertRaises(NameError) as exc_mgr: + Validation('/tmp/foo').get_formated_data + self.assertEqual('No metadata found in validation foo', + str(exc_mgr.exception)) + @mock.patch('six.moves.builtins.open') def test_validation_not_found(self, mock_open): mock_open.side_effect = IOError() diff --git a/validations_libs/validation.py b/validations_libs/validation.py index a345b478..17607c82 100644 --- a/validations_libs/validation.py +++ b/validations_libs/validation.py @@ -22,6 +22,52 @@ LOG = logging.getLogger(__name__ + ".validation") class Validation(object): + """An object for encapsulating a validation + + Each validation is an `Ansible` playbook. Each playbook have some + ``metadata``. Here is what a minimal validation would look like: + + .. code-block:: yaml + + - hosts: webserver + vars: + metadata: + name: Hello World + description: This validation prints Hello World! + roles: + - hello-world + + As shown here, the validation playbook requires three top-level + directives: + + ``hosts``, ``vars -> metadata`` and ``roles`` + + ``hosts`` specify which nodes to run the validation on. + + The ``vars`` section serves for storing variables that are going to be + available to the `Ansible` playbook. The validations API uses the + ``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`` + metadata. Groups function similar to tags and a validation can thus be part + of many groups. Here is, for example, how to have a validation be part of + the `pre-deployment` and `hardware` groups. + + .. code-block:: yaml + + - hosts: webserver + vars: + metadata: + name: Hello World + description: This validation prints Hello World! + groups: + - pre-deployment + - hardware + roles: + - hello-world + + """ _col_keys = ['ID', 'Name', 'Description', 'Groups'] @@ -36,48 +82,211 @@ class Validation(object): except IOError: raise IOError("Validation playbook not found") + @property + def has_vars_dict(self): + """Check the presence of the vars dictionary + + .. code-block:: yaml + + - hosts: webserver + vars: <==== + metadata: + name: hello world + description: this validation prints hello world! + groups: + - pre-deployment + - hardware + roles: + - hello-world + + :return: `true` if `vars` is found, `false` if not. + :rtype: `boolean` + """ + return 'vars' in self.dict.keys() + + @property + def has_metadata_dict(self): + """Check the presence of the metadata dictionary + + .. code-block:: yaml + + - hosts: webserver + vars: + metadata: <==== + name: hello world + description: this validation prints hello world! + groups: + - pre-deployment + - hardware + roles: + - hello-world + + :return: `true` if `vars` and metadata are found, `false` if not. + :rtype: `boolean` + """ + return self.has_vars_dict and 'metadata' in self.dict['vars'].keys() + @property def get_metadata(self): - if self.dict['vars'].get('metadata'): + """Get the metadata of a validation + + :return: The validation metadata + :rtype: `dict` 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/val1.yaml' + >>> val = Validation(pl) + >>> print(val.get_metadata) + {'description': 'Val1 desc.', + 'groups': ['group1', 'group2'], + 'id': 'val1', + 'name': 'The validation val1\'s name'} + """ + if self.has_metadata_dict: self.metadata = {'id': self.id} self.metadata.update(self.dict['vars'].get('metadata')) - return self.metadata + return self.metadata + else: + raise NameError( + "No metadata found in validation {}".format(self.id) + ) @property def get_vars(self): - vars = self.dict['vars'].copy() - if vars.get('metadata'): + """Get only the variables of a validation + + :return: All the variables belonging to a validation + :rtype: `dict` 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.get_vars) + {'var_name1': 'value1', + 'var_name2': 'value2'} + """ + if self.has_metadata_dict: + vars = self.dict['vars'].copy() vars.pop('metadata') - return vars + return vars + else: + raise NameError( + "No metadata found in validation {}".format(self.id) + ) @property def get_data(self): + """Get the full contents of a validation playbook + + :return: The full content of the playbook + :rtype: `dict` + + :Example: + + >>> pl = '/foo/bar/val.yaml' + >>> val = Validation(pl) + >>> print(val.get_data) + {'gather_facts': True, + 'hosts': 'all', + 'roles': ['val_role'], + 'vars': {'metadata': {'description': 'description of val ', + 'groups': ['group1', 'group2'], + 'name': 'validation one'}, + 'var_name1': 'value1'}} + """ return self.dict @property def groups(self): - return self.dict['vars']['metadata'].get('groups') + """Get the validation list of groups + + :return: A list of groups 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.groups) + ['group1', 'group2'] + """ + if self.has_metadata_dict: + groups = self.dict['vars']['metadata'].get('groups') + if groups: + return groups + else: + return [] + else: + raise NameError( + "No metadata found in validation {}".format(self.id) + ) @property def get_id(self): + """Get the validation id + + :return: The validation id + :rtype: `string` + + :Example: + + >>> pl = '/foo/bar/check-cpu.yaml' + >>> val = Validation(pl) + >>> print(val.id) + 'check-cpu' + """ return self.id @property def get_ordered_dict(self): + """Get the full ordered content of a validation + + :return: An `OrderedDict` with the full data of a validation + :rtype: `OrderedDict` + """ data = OrderedDict() data.update(self.dict) return data @property def get_formated_data(self): + """Get basic information from a validation for output display + + :return: Basic information of a validation including the `Description`, + the list of `Groups`, the `ID` and the `Name`. + :rtype: `dict` + :raise: A `NameError` exception if no metadata has been found in the + playbook + + :Example: + + >>> pl = '/foo/bar/val.yaml' + >>> val = Validation(pl) + >>> print(val.get_data) + {'Description': 'description of val', + 'Groups': ['group1', 'group2'], + 'ID': 'val', + 'Name': 'validation one'} + """ data = {} - for key in self.get_metadata.keys(): - if key in map(str.lower, self._col_keys): - for k in self._col_keys: - if key == k.lower(): - output_key = k - data[output_key] = self.get_metadata.get(key) - else: - # Get all other values: - data[key] = self.get_metadata.get(key) + metadata = self.get_metadata + if metadata: + for key in metadata.keys(): + if key in map(str.lower, self._col_keys): + for k in self._col_keys: + if key == k.lower(): + output_key = k + data[output_key] = self.get_metadata.get(key) + else: + # Get all other values: + data[key] = self.get_metadata.get(key) + return data