diff --git a/requirements.txt b/requirements.txt index 341539910..31fa50052 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,3 +22,4 @@ keystonemiddleware>=2.3.0 stevedore>=1.5.0 # Apache-2.0 exrex>=0.9.4 voluptuous>=0.8.8 +sympy>=0.7.6.1 diff --git a/test-requirements.txt b/test-requirements.txt index 933e2fba4..1bf413034 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -24,3 +24,4 @@ testtools>=1.4.0 exrex>=0.9.4 stevedore>=1.5.0 # Apache-2.0 voluptuous>=0.8.8 +sympy>=0.7.6.1 diff --git a/vitrage/evaluator/scenario.py b/vitrage/evaluator/scenario.py deleted file mode 100644 index 6a04a89bb..000000000 --- a/vitrage/evaluator/scenario.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright 2016 - Nokia -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - - -class Scenario(object): - - TYPE_ENTITY = 'entity' - TYPE_RELATE = 'relationship' - - def __init__(self): - pass - - def get_condition(self): - """Returns the condition for this scenario. - - Each condition should be formatted in 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 - For details: https://en.wikipedia.org/wiki/Disjunctive_normal_form - - :return: condition - """ - entity = 'replace with vertex' - - relationship = 'replace with edge' - - mock_entity = (entity, self.TYPE_ENTITY, True) - mock_relationship = (relationship, self.TYPE_RELATE, False) - - # single "and" clause between entity and relationship - return [(mock_entity, mock_relationship)] - - def get_actions(self): - """Returns the action specifications for this scenario. - - :return: list of actions to perform - :rtype: ActionSpecs - """ - action_spec = ActionSpecs() - return [action_spec] - - -class ActionSpecs(object): - - def get_type(self): - return 'action type str' - - def get_targets(self): - """Returns dict of template_ids to apply action on - - :return: dict of string:template_id - :rtype: dict - """ - - # e.g., for adding edge, need two ids. for alarms, will need only one. - return {'source': 'source template_id', - 'target': 'target template_id'} - - def get_properties(self): - """Returns the properties relevant to the action. - - :return: dictionary of properties relevant to the action. - :rtype: dict - """ - return {'prop_key': 'prop_val'} diff --git a/vitrage/evaluator/scenario_repository.py b/vitrage/evaluator/scenario_repository.py new file mode 100644 index 000000000..e36b28ae4 --- /dev/null +++ b/vitrage/evaluator/scenario_repository.py @@ -0,0 +1,73 @@ +# Copyright 2016 - Nokia +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from oslo_log import log + +from vitrage.common import file_utils +from vitrage.evaluator.template import Template +from vitrage.evaluator.template_syntax_validator import syntax_validate + + +LOG = log.getLogger(__name__) + + +action_types = { + 'RAISE_ALARM': 'raise_alarm', + 'ADD_CAUSAL_RELATIONSHIP': 'add_causal_relationship', + 'SET_STATE': 'set_state' +} + + +class ScenarioRepository(object): + + def __init__(self, conf): + self._load_templates_files(conf) + self.scenarios = {} + + def add_template(self, template_definition): + + if syntax_validate(template_definition): + template = Template(template_definition) + print(template) + + def get_relevant_scenarios(self, element_before, element_now): + """Returns scenarios triggered by an event. + + Returned scenarios are divided into two disjoint lists, based on the + element state (before/now) that triggered the scenario condition. + + Note that this should intuitively mean that the "before" scenarios will + activate their "undo" operation, while the "now" will activate the + "execute" operation. + + :param element_before: + :param element_now: + :return: + :rtype: dict + """ + + # trigger_id_before = 'template_id of trigger for before scenario' + # trigger_id_now = 'template_id of trigger for now scenario' + + # return {'before': [(scenario., trigger_id_before)], + # 'now': [(scenario.Scenario, trigger_id_now)]} + + pass + + def _load_templates_files(self, conf): + + templates_dir_path = conf.evaluator.templates_dir + template_definitions = file_utils.load_yaml_files(templates_dir_path) + + for template_definition in template_definitions: + self.add_template(template_definition) diff --git a/vitrage/evaluator/template.py b/vitrage/evaluator/template.py index 584d3970b..c93155ef6 100644 --- a/vitrage/evaluator/template.py +++ b/vitrage/evaluator/template.py @@ -11,11 +11,217 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +from collections import namedtuple from oslo_log import log +from sympy.logic.boolalg import And +from sympy.logic.boolalg import Not +from sympy.logic.boolalg import Or +from sympy.logic.boolalg import to_dnf +from sympy import Symbol + +from vitrage.evaluator.template_fields import TemplateFields as TFields +from vitrage.graph import Edge +from vitrage.graph import Vertex LOG = log.getLogger(__name__) +ConditionVar = namedtuple('ConditionVar', ['element', 'type', 'positive']) +ActionSpecs = namedtuple('ActionSpecs', ['type', 'targets', 'properties']) +Scenario = namedtuple('Scenario', ['condition', 'actions']) + + +TYPE_ENTITY = 'entity' +TYPE_RELATIONSHIP = 'relationship' + + class Template(object): - pass + + def __init__(self, template_def): + + super(Template, self).__init__() + + self.template_name = template_def[TFields.METADATA][TFields.ID] + + definitions = template_def[TFields.DEFINITIONS] + self.entities = self._build_entities(definitions[TFields.ENTITIES]) + self.relationships = self._build_relationships( + definitions[TFields.RELATIONSHIPS]) + + self.scenarios = self._build_scenarios(template_def[TFields.SCENARIOS]) + + @property + def entities(self): + return self._entities + + @entities.setter + def entities(self, entities): + self._entities = entities + + @property + def relationships(self): + return self._relationships + + @relationships.setter + def relationships(self, relationships): + self._relationships = relationships + + def _build_entities(self, entities_definitions): + + entities = {} + for entity_definition in entities_definitions: + + entity_dict = entity_definition[TFields.ENTITY] + template_id = entity_dict[TFields.TEMPLATE_ID] + entities[template_id] = Vertex(template_id, entity_dict) + + return entities + + def _build_relationships(self, relationships_defs): + + relationships = {} + for relationship_def in relationships_defs: + + relationship_dict = relationship_def[TFields.RELATIONSHIP] + relationship = self._create_edge(relationship_dict) + template_id = relationship_dict[TFields.TEMPLATE_ID] + relationships[template_id] = relationship + + return relationships + + def _create_edge(self, relationship_dict): + + return Edge(relationship_dict[TFields.SOURCE], + relationship_dict[TFields.TARGET], + relationship_dict[TFields.RELATIONSHIP_TYPE], + relationship_dict) + + def _build_scenarios(self, scenarios_defs): + + scenarios = [] + for scenarios_def in scenarios_defs: + + scenario_dict = scenarios_def[TFields.SCENARIO] + condition = self._parse_condition(scenario_dict[TFields.CONDITION]) + action_specs = self._build_actions(scenario_dict[TFields.ACTIONS]) + scenarios.append(Scenario(condition, action_specs)) + + return scenarios + + def _build_actions(self, actions_def): + + actions = [] + for action_def in actions_def: + action_dict = action_def[TFields.ACTION] + + action_type = action_dict[TFields.ACTION_TYPE] + + target_def = action_dict[TFields.ACTION_TARGET] + targets = self._extract_action_target(target_def) + + properties = {} + if TFields.PROPERTIES in action_dict: + properties = action_dict[TFields.PROPERTIES] + + actions.append(ActionSpecs(action_type, targets, properties)) + + return actions + + def _extract_action_target(self, action_target): + + targets = {} + + for key, value in action_target.iteritems(): + targets[key] = self._extract_variable(value) + + return targets + + def _parse_condition(self, condition_str): + """Parse condition string into an object + + The condition object is formatted in 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)] + + :param condition_str: the string as it written in the template itself + :return: Condition object + """ + + condition_dnf = self.convert_to_dnf_format(condition_str) + + if isinstance(condition_dnf, Or): + return self._extract_or_condition(condition_dnf) + + if isinstance(condition_dnf, And): + return [self._extract_and_condition(condition_dnf)] + + if isinstance(condition_dnf, Not): + return [(self._extract_not_condition(condition_dnf))] + + if isinstance(condition_dnf, Symbol): + return [(self._extract_condition_variable(condition_dnf, False))] + + def convert_to_dnf_format(self, condition_str): + + condition_str = condition_str.replace('and', '&') + condition_str = condition_str.replace('or', '|') + condition_str = condition_str.replace('not ', '~') + + return to_dnf(condition_str) + + def _extract_or_condition(self, or_condition): + + variables = [] + for variable in or_condition.args: + + if isinstance(variable, And): + variables.append((self._extract_and_condition(variable)),) + elif isinstance(variable, Not): + variables.append((self._extract_not_condition(variable),)) + else: + variables.append((self._extract_condition_variable(variable, + False),)) + return variables + + def _extract_and_condition(self, and_condition): + + variables = () + for arg in and_condition.args: + if isinstance(arg, Not): + condition_var = self._extract_not_condition(arg) + else: + condition_var = self._extract_condition_variable(arg, False) + variables = variables + condition_var + + return variables + + def _extract_not_condition(self, not_condition): + self._extract_condition_variable(not_condition.args, True) + + def _extract_condition_variable(self, symbol, not_): + + template_id = symbol.__str__() + variable = self._extract_variable(template_id) + + if variable: + return ConditionVar(variable[0], variable[1], not_) + + return None + + def _extract_variable(self, template_id): + + if template_id in self.relationships: + return self.relationships[template_id], TYPE_RELATIONSHIP + + if template_id in self.entities: + return self.entities[template_id], TYPE_ENTITY + + LOG.error('Cannot find template_id = %s in template named: %s' % + (template_id, self.template_name)) + return None diff --git a/vitrage/evaluator/template_loader.py b/vitrage/evaluator/template_content_validator.py similarity index 58% rename from vitrage/evaluator/template_loader.py rename to vitrage/evaluator/template_content_validator.py index 5d35a3f77..4201dcbb3 100644 --- a/vitrage/evaluator/template_loader.py +++ b/vitrage/evaluator/template_content_validator.py @@ -1,27 +1,35 @@ -# Copyright 2016 - Nokia +# Copyright 2015 - Nokia # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + from oslo_log import log -from vitrage.common import file_utils LOG = log.getLogger(__name__) -def load_templates_files(conf): +def syntax_validate(template_conf): + pass - templates_dir_path = conf.evaluator.templates_dir - template_files = file_utils.load_yaml_files(templates_dir_path) - for template_file in template_files: - pass +def validate_scenario_condition(condition_str): + """Validate the condition content. + + Check: + 1. The brackets are valid + + :param condition: the condition string + :return: True if the condition itself is valid, otherwise returns False + :rtype: bool + """ + pass diff --git a/vitrage/evaluator/template_query_utils.py b/vitrage/evaluator/template_query_utils.py deleted file mode 100644 index 1b8fb327c..000000000 --- a/vitrage/evaluator/template_query_utils.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2016 - Nokia -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import vitrage.evaluator.scenario as scenario - - -class ScenarioManager(object): - - def get_relevant_scenarios(self, element_before, element_now): - """Returns scenarios triggered by an event. - - Returned scenarios are divided into two disjoint lists, based on the - element state (before/now) that triggered the scenario condition. - - Note that this should intuitively mean that the "before" scenarios will - activate their "undo" operation, while the "now" will activate the - "execute" operation. - - :param element_before: - :param element_now: - :return: - :rtype: dict - """ - - trigger_id_before = 'template_id of trigger for before scenario' - trigger_id_now = 'template_id of trigger for now scenario' - - return {'before': [(scenario.Scenario(), trigger_id_before)], - 'now': [(scenario.Scenario(), trigger_id_now)]} diff --git a/vitrage/evaluator/template_validator.py b/vitrage/evaluator/template_syntax_validator.py similarity index 99% rename from vitrage/evaluator/template_validator.py rename to vitrage/evaluator/template_syntax_validator.py index a3c55f11a..286866100 100644 --- a/vitrage/evaluator/template_validator.py +++ b/vitrage/evaluator/template_syntax_validator.py @@ -32,7 +32,7 @@ DICT_STRUCTURE_SCHEMA_ERROR = '%s must refer to dictionary.' SCHEMA_CONTENT_ERROR = '%s must contain %s Fields.' -def validate(template_conf): +def syntax_validate(template_conf): is_valid = validate_template_sections(template_conf) diff --git a/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml b/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml index 23dc1f851..e0bca31cb 100644 --- a/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml +++ b/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml @@ -39,13 +39,13 @@ scenarios: condition: alarm_on_host and host_contains_instance actions: - action: - action_type: raise_alarm + action_type: RAISE_ALARM properties: alarm_type: VM_CPU_SUBOPTIMAL_PERFORMANCE action_target: target: 4 - action: - action_type: set_state + action_type: SET_STATE properties: state: SUBOPTIMAL action_target: @@ -54,7 +54,7 @@ scenarios: condition: alarm_on_host and alarm_on_instance and host_contains_instance actions: - action: - action_type: add_causal_relationship + action_type: ADD_CAUSAL_RELATIONSHIP action_target: source: 1 target: 2 diff --git a/vitrage/tests/unit/evaluator/test_template_loader.py b/vitrage/tests/unit/evaluator/test_scenario_repository.py similarity index 67% rename from vitrage/tests/unit/evaluator/test_template_loader.py rename to vitrage/tests/unit/evaluator/test_scenario_repository.py index dd7c46d0c..d8beeb17b 100644 --- a/vitrage/tests/unit/evaluator/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/test_scenario_repository.py @@ -11,10 +11,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. - from oslo_config import cfg from oslo_log import log as logging -from vitrage.common import file_utils +from vitrage.evaluator.scenario_repository import ScenarioRepository + from vitrage.tests import base from vitrage.tests.mocks import utils @@ -22,7 +22,7 @@ from vitrage.tests.mocks import utils LOG = logging.getLogger(__name__) -class TemplateLoaderTest(base.BaseTest): +class ScenarioRepositoryTest(base.BaseTest): OPTS = [ cfg.StrOpt('templates_dir', @@ -30,17 +30,12 @@ class TemplateLoaderTest(base.BaseTest): ), ] - def setUp(self): - super(TemplateLoaderTest, self).setUp() + @classmethod + def setUpClass(cls): - self.template_dir_path = utils.get_resources_dir() + '/templates' - - self.conf = cfg.ConfigOpts() - self.conf.register_opts(self.OPTS, group='evaluator') - - self.template_yamls = file_utils.load_yaml_files( - self.template_dir_path - ) + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group='evaluator') def test_template_loader(self): - pass + repository = ScenarioRepository(self.conf) + print(repository) diff --git a/vitrage/tests/unit/evaluator/test_template_validator.py b/vitrage/tests/unit/evaluator/test_template_validator.py index e3cd17aa4..f4bf3c5c8 100644 --- a/vitrage/tests/unit/evaluator/test_template_validator.py +++ b/vitrage/tests/unit/evaluator/test_template_validator.py @@ -17,7 +17,7 @@ from oslo_log import log as logging from vitrage.common import file_utils from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator import template_validator +from vitrage.evaluator import template_syntax_validator from vitrage.tests import base from vitrage.tests.mocks import utils @@ -40,37 +40,38 @@ class TemplateValidatorTest(base.BaseTest): return copy.deepcopy(self.first_template) def test_template_validator(self): - self.assertTrue(template_validator.validate(self.first_template)) + self.assertTrue(template_syntax_validator.syntax_validate( + self.first_template)) def test_validate_template_without_metadata_section(self): template = self.clone_template template.pop(TemplateFields.METADATA) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_without_id_in_metadata_section(self): template = self.clone_template template[TemplateFields.METADATA].pop(TemplateFields.ID) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_without_definitions_section(self): template = self.clone_template template.pop(TemplateFields.DEFINITIONS) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_without_entities(self): template = self.clone_template template[TemplateFields.DEFINITIONS].pop(TemplateFields.ENTITIES) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_with_empty_entities(self): template = self.clone_template template[TemplateFields.DEFINITIONS][TemplateFields.ENTITIES] = [] - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_entity_without_required_fields(self): @@ -78,13 +79,13 @@ class TemplateValidatorTest(base.BaseTest): definitions = template[TemplateFields.DEFINITIONS] entity = definitions[TemplateFields.ENTITIES][0] entity[TemplateFields.ENTITY].pop(TemplateFields.CATEGORY) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) template = self.clone_template definitions = template[TemplateFields.DEFINITIONS] entity = definitions[TemplateFields.ENTITIES][0] entity[TemplateFields.ENTITY].pop(TemplateFields.TEMPLATE_ID) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_relationships_without_required_fields(self): @@ -92,13 +93,13 @@ class TemplateValidatorTest(base.BaseTest): definitions = template[TemplateFields.DEFINITIONS] relationship = definitions[TemplateFields.RELATIONSHIPS][0] relationship[TemplateFields.RELATIONSHIP].pop(TemplateFields.SOURCE) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) template = self.clone_template definitions = template[TemplateFields.DEFINITIONS] relationship = definitions[TemplateFields.RELATIONSHIPS][0] relationship[TemplateFields.RELATIONSHIP].pop(TemplateFields.TARGET) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) template = self.clone_template definitions = template[TemplateFields.DEFINITIONS] @@ -106,37 +107,37 @@ class TemplateValidatorTest(base.BaseTest): relationship[TemplateFields.RELATIONSHIP].pop( TemplateFields.TEMPLATE_ID ) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_without_scenarios(self): template = self.clone_template template.pop(TemplateFields.SCENARIOS) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_with_empty_scenarios(self): template = self.clone_template template[TemplateFields.SCENARIOS] = [] - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_scenario_without_required_fields(self): template = self.clone_template scenario = template[TemplateFields.SCENARIOS][0] scenario[TemplateFields.SCENARIO].pop(TemplateFields.CONDITION) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) template = self.clone_template scenario = template[TemplateFields.SCENARIOS][0] scenario[TemplateFields.SCENARIO].pop(TemplateFields.ACTIONS) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_template_with_empty_actions(self): template = self.clone_template scenario = template[TemplateFields.SCENARIOS][0] scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS] = [] - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) def test_validate_action_without_required_fields(self): @@ -144,10 +145,10 @@ class TemplateValidatorTest(base.BaseTest): scenario = template[TemplateFields.SCENARIOS][0] action = scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS][0] action[TemplateFields.ACTION].pop(TemplateFields.ACTION_TYPE) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template)) template = self.clone_template scenario = template[TemplateFields.SCENARIOS][0] action = scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS][0] action[TemplateFields.ACTION].pop(TemplateFields.ACTION_TARGET) - self.assertFalse(template_validator.validate(template)) + self.assertFalse(template_syntax_validator.syntax_validate(template))