From 86691304c81045522834e0f48566dd36f8ab1202 Mon Sep 17 00:00:00 2001 From: Yujun Zhang Date: Wed, 3 May 2017 14:34:23 +0800 Subject: [PATCH] Add document about implementation details of template loading - add strict tests in template data verification to help understand - add traceback in scenario evaluator design document - rename example templates with a more meaningful name Change-Id: Ie5a9c34fcd6fdac3bf9d4552a0c92fb569c075fc --- doc/source/scenario-evaluator.rst | 7 +- doc/source/templates-loading.rst | 195 ++++++++++++++++++ vitrage/evaluator/template_data.py | 12 +- .../{new.yaml => high_availability.yaml} | 0 .../unit/evaluator/test_template_data.py | 67 ++++++ 5 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 doc/source/templates-loading.rst rename vitrage/tests/resources/templates/evaluator/{new.yaml => high_availability.yaml} (100%) diff --git a/doc/source/scenario-evaluator.rst b/doc/source/scenario-evaluator.rst index d6dd7a8df..2ceb663dd 100644 --- a/doc/source/scenario-evaluator.rst +++ b/doc/source/scenario-evaluator.rst @@ -95,7 +95,10 @@ Templates should all be located in the */templates* folder. When Vitrage is started up, all the templates are loaded into a *Scenario* *Repository*. During this loading, template verification is done to ensure the correct format is used, references are valid, and more. Errors in -each template should be written to the log. Invalid templates are skipped. +each template should be written to the log. Invalid templates are skipped. See +details_. + +.. _details: templates-loading.html The Scenario Repository supports querying for scenarios based on a vertex or edge in the Entity Graph. Given such a graph element, the Scenario Repository @@ -190,4 +193,4 @@ overlap in their effect. For more details on the implementation of this functionality, see the design on this etherpad_. -.. _etherpad: https://etherpad.openstack.org/p/vitrage-overlapping-templates-support-design \ No newline at end of file +.. _etherpad: https://etherpad.openstack.org/p/vitrage-overlapping-templates-support-design diff --git a/doc/source/templates-loading.rst b/doc/source/templates-loading.rst new file mode 100644 index 000000000..5cae2352d --- /dev/null +++ b/doc/source/templates-loading.rst @@ -0,0 +1,195 @@ +======================== +Vitrage Template Loading +======================== + +Overview +======== + +Vitrage templates are defined in yaml with specific format_. During startup, +templates are loaded into ``TemplateData``. After that , scenarios in loaded +templates will be added into scenario repository. + +This document explains the implementation details of template data to help +developer understand how scenario_evaluator_ works. + +.. _format: vitrage-template-format.html +.. _scenario_evaluator: scenario-evaluator.html + +Example +======= + +Let's take a basic template as example + +.. code-block:: yaml + + metadata: + name: basic_template + description: basic template for general tests + definitions: + entities: + - entity: + category: ALARM + type: nagios + name: HOST_HIGH_CPU_LOAD + template_id: alarm + - entity: + category: RESOURCE + type: nova.host + template_id: resource + relationships: + - relationship: + source: alarm + target: resource + relationship_type: on + template_id : alarm_on_host + scenarios: + - scenario: + condition: alarm_on_host + actions: + - action: + action_type: set_state + properties: + state: SUBOPTIMAL + action_target: + target: resource + +``TemplateData`` will build ``entites``, ``relationships`` and most importantly +``scenarios`` out from the definition. + +.. code-block:: python + + expected_entities = { + 'alarm': Vertex(vertex_id='alarm', + properties={'category': 'ALARM', + 'type': 'nagios', + 'name': 'HOST_HIGH_CPU_LOAD', + 'is_deleted': False, + 'is_placeholder': False + }), + 'resource': Vertex(vertex_id='resource', + properties={'category': 'RESOURCE', + 'type': 'nova.host', + 'is_deleted': False, + 'is_placeholder': False + }) + } + + expected_relationships = { + 'alarm_on_host': EdgeDescription( + edge=Edge( + source_id='alarm', + target_id='resource', + label='on', + properties={ + 'relationship_type': 'on', + 'is_deleted': False, + 'negative_condition': False}), + source=expected_entities['alarm'], + target=expected_entities['resource'] + ) + } + + expected_scenario = Scenario( + id='basic_template-scenario0', + condition=[ + [ConditionVar( + variable=expected_relationships['alarm_on_host'], + type='relationship', + positive=True)] + ], + actions=[ + ActionSpecs( + type='set_state', + targets={ + 'target': 'resource'}, + properties={ + 'state': 'SUBOPTIMAL'})], + subgraphs=[object] # [] + ) + +Entities and relationships +========================== + +Entities and relationships are loaded into dicts keyed by ``template_id`` so +that the references in scenarios can be resolved quickly. + +Note that entities and relationships dicts are **NOT** added to scenario +repository. This implies the scope of ``template_id`` is restricted to one +template file. It is **NOT** global. + +It is considered invalid to have duplicated ``template_id`` in one template, but +it is possible that two or more entities have exactly the same properties except +``template_id``. There is an example in +``vitrage/tests/templates/evaluator/high_availability.yaml``: + +.. code:: yaml + + - entity: + category: RESOURCE + type: nova.instance + template_id: instance1 + - entity: + category: RESOURCE + type: nova.instance + template_id: instance2 + +It is used to model scenario contains two or more entities of same type, such +as high availability condition. + +Scenarios +========= + +``Scenario`` are defined as a ``namedtuple`` + +.. code-block:: python + + Scenario = namedtuple('Scenario', ['id', 'condition', 'actions', 'subgraphs']) + +id +-- + +Formatted from template name and scenario index + +condition +--------- + +Condition strings in template are expressions composed of template id and +operators. As explained in embedded comment: + + The condition string will be converted here into DNF (Disjunctive + Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)... + where X, Y, Z, V, W are either entities or relationships + more details: https://en.wikipedia.org/wiki/Disjunctive_normal_form + + The condition variable lists is then extracted from the DNF object. It + is a list of lists. Each inner list represents an AND expression + compound condition variables. The outer list presents the OR expression + + [[and_var1, and_var2, ...], or_list_2, ...] + + :param condition_str: the string as it written in the template itself + :return: condition_vars_lists + +Each condition variable refers either an entity or relationship by ``variable``. +The same variable in different conditions share an identical object. It is not +duplicated. + +actions +------- + +``actions`` is a list of ``ActionSpecs``. + +Note that the values of action ``targets`` are **string**. It is different from +how relationships and entities are referred in ``condition``, which are object +references. + +The targets values are linked to ``vertex_id`` of entity condition variables or +``source_id`` and ``target_id`` in a relationship condition variable. It will +be resolved to real targets from matched sub-graph in entity graph. + +subgraphs +--------- + +Sub graphs are compiled from conditions for pattern matching in the entity +graph. Each sub-list in condition variables list is compiled into one sub +graph. The actions will be triggered if any of the subgraph is matched. diff --git a/vitrage/evaluator/template_data.py b/vitrage/evaluator/template_data.py index 5911f0f4b..97eccc613 100644 --- a/vitrage/evaluator/template_data.py +++ b/vitrage/evaluator/template_data.py @@ -203,17 +203,19 @@ class TemplateData(object): def _parse_condition(self, condition_str): """Parse condition string into an object - The condition object will be converted here into DNF (Disjunctive + The condition string will be converted here into DNF (Disjunctive Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)... where X, Y, Z, V, W are either entities or relationships more details: https://en.wikipedia.org/wiki/Disjunctive_normal_form - The condition object itself is a list of tuples. each tuple represents - an AND expression compound ConditionElements. The list presents the - OR expression e.g. [(condition_element1, condition_element2)] + The condition variable lists is then extracted from the DNF object. It + is a list of lists. Each inner list represents an AND expression + compound condition variables. The outer list presents the OR expression + + [[and_var1, and_var2, ...], or_list_2, ...] :param condition_str: the string as it written in the template itself - :return: Condition object + :return: condition_vars_lists """ condition_dnf = self.convert_to_dnf_format(condition_str) diff --git a/vitrage/tests/resources/templates/evaluator/new.yaml b/vitrage/tests/resources/templates/evaluator/high_availability.yaml similarity index 100% rename from vitrage/tests/resources/templates/evaluator/new.yaml rename to vitrage/tests/resources/templates/evaluator/high_availability.yaml diff --git a/vitrage/tests/unit/evaluator/test_template_data.py b/vitrage/tests/unit/evaluator/test_template_data.py index 42b255760..39f38c418 100644 --- a/vitrage/tests/unit/evaluator/test_template_data.py +++ b/vitrage/tests/unit/evaluator/test_template_data.py @@ -13,10 +13,13 @@ # under the License. from vitrage.common.constants import EdgeLabel +from vitrage.evaluator.template_data import ActionSpecs from vitrage.evaluator.template_data import ConditionVar from vitrage.evaluator.template_data import EdgeDescription +from vitrage.evaluator.template_data import Scenario from vitrage.evaluator.template_data import TemplateData from vitrage.evaluator.template_fields import TemplateFields as TFields +from vitrage.graph import Edge from vitrage.graph import Vertex from vitrage.tests import base from vitrage.tests.mocks import utils @@ -48,6 +51,70 @@ class BasicTemplateTest(base.BaseTest): self._validate_relationships(relationships, relate_def, entities) self._validate_scenarios(scenarios, entities) + expected_entities = { + 'alarm': Vertex(vertex_id='alarm', + properties={'category': 'ALARM', + 'type': 'nagios', + 'name': 'HOST_HIGH_CPU_LOAD', + 'is_deleted': False, + 'is_placeholder': False + }), + 'resource': Vertex(vertex_id='resource', + properties={'category': 'RESOURCE', + 'type': 'nova.host', + 'is_deleted': False, + 'is_placeholder': False + }) + } + + expected_relationships = { + 'alarm_on_host': EdgeDescription( + edge=Edge(source_id='alarm', + target_id='resource', + label='on', + properties={'relationship_type': 'on', + 'is_deleted': False, + 'negative_condition': False}), + source=expected_entities['alarm'], + target=expected_entities['resource'] + ) + } + + expected_scenario = Scenario( + id='basic_template-scenario0', + condition=[ + [ConditionVar(variable=expected_relationships['alarm_on_host'], + type='relationship', + positive=True)]], + actions=[ + ActionSpecs( + type='set_state', + targets={'target': 'resource'}, + properties={'state': 'SUBOPTIMAL'})], + subgraphs=template_data.scenarios[0].subgraphs + ) + + self._validate_strict_equal(template_data, + expected_entities, + expected_relationships, + expected_scenario) + + def _validate_strict_equal(self, + template_data, + expected_entities, + expected_relationships, + expected_scenario + ): + self.assert_dict_equal(expected_entities, template_data.entities, + 'entities not equal') + + self.assert_dict_equal(expected_relationships, + template_data.relationships, + 'relationship not equal') + + self.assertEqual(expected_scenario, template_data.scenarios[0], + 'scenario not equal') + def _validate_entities(self, entities, entities_def): self.assertIsNotNone(entities)