From a859882b470c9f8062ba3a6639bdd701ce133963 Mon Sep 17 00:00:00 2001 From: Idan Hefetz Date: Thu, 24 Jan 2019 13:01:40 +0000 Subject: [PATCH] Template version 3 - short format Added version 3 syntax and content validation and a new loader, with unitests. Story: 2004871 Task: 29130 Depends-On: https://review.openstack.org/#/c/633410/ Change-Id: Ia6935b561b4123e99e35f46d6a2a07267edd92ea --- vitrage/evaluator/base.py | 15 + vitrage/evaluator/condition.py | 9 +- vitrage/evaluator/scenario_repository.py | 10 +- vitrage/evaluator/template_data.py | 9 +- vitrage/evaluator/template_fields.py | 1 + .../template_loading/scenario_loader.py | 91 ++-- .../template_loading/template_loader.py | 32 +- .../template_loading/template_loader_v3.py | 158 ++++++ .../template_loading/v1/action_loader.py | 4 +- .../evaluator/template_loading/v3/__init__.py | 0 .../template_loading/v3/action_loader.py | 35 ++ vitrage/evaluator/template_schemas.py | 35 ++ .../evaluator/template_validation/__init__.py | 21 + vitrage/evaluator/template_validation/base.py | 23 +- .../content/template_content_validator_v3.py | 135 ++++++ .../template_validation/status_messages.py | 16 +- .../template_syntax_validator.py | 21 +- .../template_syntax_validator_v3.py | 137 ++++++ vitrage/graph/filter.py | 6 +- vitrage/tests/mocks/graph_generator.py | 5 + .../templates/v3_templates/valid_actions.yaml | 63 +++ .../v3_templates/valid_conditions.yaml | 58 +++ vitrage/tests/unit/evaluator/__init__.py | 21 + .../test_template_loading_v3.py | 450 ++++++++++++++++++ .../v2/test_template_loader.py | 4 +- .../test_template_validator_v3.py | 346 ++++++++++++++ .../tests/unit/evaluator/test_condition.py | 4 +- .../unit/evaluator/test_template_loader.py | 19 +- 28 files changed, 1607 insertions(+), 121 deletions(-) create mode 100644 vitrage/evaluator/template_loading/template_loader_v3.py create mode 100644 vitrage/evaluator/template_loading/v3/__init__.py create mode 100644 vitrage/evaluator/template_loading/v3/action_loader.py create mode 100644 vitrage/evaluator/template_validation/content/template_content_validator_v3.py create mode 100644 vitrage/evaluator/template_validation/template_syntax_validator_v3.py create mode 100644 vitrage/tests/resources/templates/v3_templates/valid_actions.yaml create mode 100644 vitrage/tests/resources/templates/v3_templates/valid_conditions.yaml create mode 100644 vitrage/tests/unit/evaluator/template_loading/test_template_loading_v3.py create mode 100644 vitrage/tests/unit/evaluator/template_validation/test_template_validator_v3.py diff --git a/vitrage/evaluator/base.py b/vitrage/evaluator/base.py index a58370fe1..c72ac7eb6 100644 --- a/vitrage/evaluator/base.py +++ b/vitrage/evaluator/base.py @@ -14,7 +14,13 @@ from collections import namedtuple import re +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory + Template = namedtuple('Template', ['uuid', 'data', 'date']) +TEMPLATE_LOADER = 'template_loader' +SYNTAX = 'syntax' +CONTENT = 'content' def is_function(str): @@ -24,3 +30,12 @@ def is_function(str): Search for a regex with open and close parenthesis """ return re.match('.*\(.*\)', str) + + +def get_template_schema(template): + metadata = template.get(TemplateFields.METADATA) + if metadata is None: + return None + version = metadata.get(TemplateFields.VERSION, + TemplateSchemaFactory.DEFAULT_VERSION) + return TemplateSchemaFactory().template_schema(version) diff --git a/vitrage/evaluator/condition.py b/vitrage/evaluator/condition.py index 9d7c60a15..910808bad 100644 --- a/vitrage/evaluator/condition.py +++ b/vitrage/evaluator/condition.py @@ -17,7 +17,7 @@ from collections import namedtuple 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 as sympy_to_dfn +from sympy.logic.boolalg import to_dnf as sympy_to_dnf from sympy import Symbol @@ -149,13 +149,16 @@ def parse_condition(condition_str): def convert_to_dnf_format(condition_str): - condition_str = condition_str.replace(' and ', '&') + condition_str = condition_str.replace(' AND ', '&') condition_str = condition_str.replace(' or ', '|') + condition_str = condition_str.replace(' OR ', '|') condition_str = condition_str.replace(' not ', '~') + condition_str = condition_str.replace(' NOT ', '~') condition_str = condition_str.replace('not ', '~') + condition_str = condition_str.replace('NOT ', '~') - return sympy_to_dfn(condition_str) + return sympy_to_dnf(condition_str) def extract_or_condition(or_condition): diff --git a/vitrage/evaluator/scenario_repository.py b/vitrage/evaluator/scenario_repository.py index 757b86f91..f58259d07 100644 --- a/vitrage/evaluator/scenario_repository.py +++ b/vitrage/evaluator/scenario_repository.py @@ -20,11 +20,12 @@ from oslo_log import log from vitrage.common.constants import TemplateStatus from vitrage.common.constants import TemplateTypes as TType from vitrage.common.utils import get_portion +from vitrage.evaluator.base import get_template_schema from vitrage.evaluator.base import Template +from vitrage.evaluator.base import TEMPLATE_LOADER from vitrage.evaluator.equivalence_repository import EquivalenceRepository from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_loading.scenario_loader import ScenarioLoader -from vitrage.evaluator.template_loading.template_loader import TemplateLoader from vitrage.evaluator.template_validation.template_syntax_validator import \ EXCEPTION from vitrage.graph.filter import check_filter as check_subset @@ -104,8 +105,11 @@ class ScenarioRepository(object): self.templates[template.uuid] = Template(template.uuid, template.file_content, template.created_at) - template_data = TemplateLoader().load(template.file_content, - self._def_templates) + schema = get_template_schema(template.file_content) + template_data = schema.loaders[TEMPLATE_LOADER].load( + schema, + template.file_content, + self._def_templates) for scenario in template_data.scenarios: for equivalent_scenario in self._expand_equivalence(scenario): self._add_scenario(equivalent_scenario) diff --git a/vitrage/evaluator/template_data.py b/vitrage/evaluator/template_data.py index d5d580ea2..c7d2c5085 100644 --- a/vitrage/evaluator/template_data.py +++ b/vitrage/evaluator/template_data.py @@ -47,8 +47,13 @@ class Scenario(object): # noinspection PyAttributeOutsideInit class TemplateData(object): - def __init__(self, name, template_type, version, entities, - relationships, scenarios): + def __init__(self, + name=None, + template_type=None, + version=None, + entities=None, + relationships=None, + scenarios=None): self.name = name self.template_type = template_type self.version = version diff --git a/vitrage/evaluator/template_fields.py b/vitrage/evaluator/template_fields.py index f6ebd0aae..8a3e19d59 100644 --- a/vitrage/evaluator/template_fields.py +++ b/vitrage/evaluator/template_fields.py @@ -25,6 +25,7 @@ class TemplateFields(TemplateTopologyFields): ACTION_TARGET = 'action_target' ACTION_TYPE = 'action_type' CATEGORY = 'category' + CAUSING_ALARM = 'causing_alarm' CONDITION = 'condition' INCLUDES = 'includes' SEVERITY = 'severity' diff --git a/vitrage/evaluator/template_loading/scenario_loader.py b/vitrage/evaluator/template_loading/scenario_loader.py index b0225b366..d4225e2ab 100644 --- a/vitrage/evaluator/template_loading/scenario_loader.py +++ b/vitrage/evaluator/template_loading/scenario_loader.py @@ -48,8 +48,10 @@ class ScenarioLoader(object): scenario_id = "%s-scenario%s" % (self.name, str(counter)) scenario_dict = scenario_def[TFields.SCENARIO] condition = parse_condition(scenario_dict[TFields.CONDITION]) - self.valid_target = \ - self._calculate_missing_action_target(condition) + self.valid_target = calculate_action_target( + condition, + self._template_entities, + self._template_relationships) actions = self._build_actions(scenario_dict[TFields.ACTIONS], scenario_id) subgraphs = SubGraphBuilder.from_condition( @@ -70,9 +72,9 @@ class ScenarioLoader(object): vertex_id=entities[template_id].vertex_id, properties={k: v for k, v in entity_props}) relationships = { - rel_id: cls._build_equivalent_relationship(rel, - template_id, - entity_props) + rel_id: _build_equivalent_relationship(rel, + template_id, + entity_props) for rel_id, rel in scenario.relationships.items()} def extract_var(symbol_name): @@ -125,48 +127,51 @@ class ScenarioLoader(object): self.entities[symbol_name] = entity return entity, ENTITY - def _calculate_missing_action_target(self, condition): - """Return a vertex that can be used as an action target. - External actions like execute_mistral do not have an explicit - action target. This parameter is a must for the sub-graph matching - algorithm. If it is missing, we would like to select an arbitrary - target from the condition. +def calculate_action_target(condition, entities, relationships): + """Return a vertex that can be used as an action target. - """ - definition_index = self._template_entities.copy() - definition_index.update(self._template_relationships) - targets = \ - get_condition_common_targets(condition, - definition_index, - self.TemplateDataSymbolResolver()) - return {TFields.TARGET: targets.pop()} if targets else None + External actions like execute_mistral do not have an explicit + action target. This parameter is a must for the sub-graph matching + algorithm. If it is missing, we would like to select an arbitrary + target from the condition. + If the target result is empty the condition is not valid. - class TemplateDataSymbolResolver(SymbolResolver): - def is_relationship(self, symbol): - return isinstance(symbol, EdgeDescription) + """ + definition_index = entities.copy() + definition_index.update(relationships) + targets = get_condition_common_targets( + condition, + definition_index, + TemplateDataSymbolResolver()) + return {TFields.TARGET: targets.pop()} if targets else None - def get_relationship_source_id(self, relationship): - return relationship.source.vertex_id - def get_relationship_target_id(self, relationship): - return relationship.target.vertex_id +class TemplateDataSymbolResolver(SymbolResolver): + def is_relationship(self, symbol): + return isinstance(symbol, EdgeDescription) - def get_entity_id(self, entity): - return entity.vertex_id + def get_relationship_source_id(self, relationship): + return relationship.source.vertex_id - @staticmethod - def _build_equivalent_relationship(relationship, - template_id, - entity_props): - source = relationship.source - target = relationship.target - if relationship.edge.source_id == template_id: - source = Vertex(vertex_id=source.vertex_id, - properties={k: v for k, v in entity_props}) - elif relationship.edge.target_id == template_id: - target = Vertex(vertex_id=target.vertex_id, - properties={k: v for k, v in entity_props}) - return EdgeDescription(source=source, - target=target, - edge=relationship.edge) + def get_relationship_target_id(self, relationship): + return relationship.target.vertex_id + + def get_entity_id(self, entity): + return entity.vertex_id + + +def _build_equivalent_relationship(relationship, + template_id, + entity_props): + source = relationship.source + target = relationship.target + if relationship.edge.source_id == template_id: + source = Vertex(vertex_id=source.vertex_id, + properties={k: v for k, v in entity_props}) + elif relationship.edge.target_id == template_id: + target = Vertex(vertex_id=target.vertex_id, + properties={k: v for k, v in entity_props}) + return EdgeDescription(source=source, + target=target, + edge=relationship.edge) diff --git a/vitrage/evaluator/template_loading/template_loader.py b/vitrage/evaluator/template_loading/template_loader.py index d6b4157c1..2740cbb3f 100644 --- a/vitrage/evaluator/template_loading/template_loader.py +++ b/vitrage/evaluator/template_loading/template_loader.py @@ -14,13 +14,11 @@ from oslo_log import log -from vitrage.common.constants import VertexProperties as VProps from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_data import TemplateData from vitrage.evaluator.template_fields import TemplateFields as TFields from vitrage.evaluator.template_loading.props_converter import PropsConverter from vitrage.evaluator.template_loading.scenario_loader import ScenarioLoader -from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory from vitrage.graph import Edge from vitrage.graph import Vertex from vitrage.utils import evaluator as evaluator_utils @@ -31,29 +29,11 @@ LOG = log.getLogger(__name__) class TemplateLoader(object): - PROPS_CONVERSION = { - 'category': VProps.VITRAGE_CATEGORY, - 'type': VProps.VITRAGE_TYPE, - 'resource_id': VProps.VITRAGE_RESOURCE_ID, - 'sample_timestamp': VProps.VITRAGE_SAMPLE_TIMESTAMP, - 'is_deleted': VProps.VITRAGE_IS_DELETED, - 'is_placeholder': VProps.VITRAGE_IS_PLACEHOLDER, - 'aggregated_state': VProps.VITRAGE_AGGREGATED_STATE, - 'operational_state': VProps.VITRAGE_OPERATIONAL_STATE, - 'aggregated_severity': VProps.VITRAGE_AGGREGATED_SEVERITY, - 'operational_severity': VProps.VITRAGE_OPERATIONAL_SEVERITY - } - def __init__(self): self.entities = {} self.relationships = {} - def load(self, template_def, def_templates=None): - - template_schema = self._get_template_schema(template_def) - if not template_schema: - LOG.error('Failed to load template - unsupported version') - return + def load(self, template_schema, template_def, def_templates=None): name = template_def[TFields.METADATA][TFields.NAME] @@ -179,13 +159,3 @@ class TemplateLoader(object): ignore_ids = [TFields.TEMPLATE_ID, TFields.SOURCE, TFields.TARGET] return \ {key: var_dict[key] for key in var_dict if key not in ignore_ids} - - @staticmethod - def _get_template_schema(template): - metadata = template.get(TFields.METADATA) - - if metadata: - version = metadata.get(TFields.VERSION) - return TemplateSchemaFactory().template_schema(version) - else: - return None diff --git a/vitrage/evaluator/template_loading/template_loader_v3.py b/vitrage/evaluator/template_loading/template_loader_v3.py new file mode 100644 index 000000000..785b70b8c --- /dev/null +++ b/vitrage/evaluator/template_loading/template_loader_v3.py @@ -0,0 +1,158 @@ +# Copyright 2019 - 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 copy + +from oslo_log import log +import re + +from vitrage.evaluator.condition import parse_condition +from vitrage.evaluator.template_data import EdgeDescription +from vitrage.evaluator.template_data import ENTITY +from vitrage.evaluator.template_data import RELATIONSHIP +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.evaluator.template_loading.props_converter import PropsConverter +from vitrage.evaluator.template_loading.scenario_loader import \ + calculate_action_target +from vitrage.evaluator.template_loading.subgraph_builder import SubGraphBuilder +from vitrage.evaluator.template_validation.base import ValidationError +from vitrage.graph import Edge +from vitrage.graph import Vertex + + +LOG = log.getLogger(__name__) + + +class TemplateLoader(object): + + def load(self, template_schema, template_def, def_templates=None): + template = copy.deepcopy(template_def) + entities = _build_entities(template) + relationships = _build_condition_relationships(template, entities) + return TemplateData(scenarios=_build_scenarios( + template, entities, relationships, template_schema)) + + +def _build_entities(template): + entities = dict() + for template_id, entity in template[TFields.ENTITIES].items(): + properties = PropsConverter.convert_props_with_dictionary(entity) + entities[template_id] = Vertex(template_id, properties) + return entities + + +def _build_condition_relationships(template, entities): + relationships = dict() + for scenario in template[TFields.SCENARIOS]: + condition = scenario.get(TFields.CONDITION) + extracted_relationships, processed_condition = \ + _process_condition(condition, entities) + relationships.update(extracted_relationships) + scenario[TFields.CONDITION] = processed_condition + + return relationships + + +def _process_condition(condition, entities): + """Process the condition, while extracting the condition relationships + + Example: + condition: 'host_alarm [ on ] host AND host [contains] instance' + + regex matches: + match group 1: 'host_alarm' + match group 2: 'on' + match group 3: 'host' + .. + + Example returns: + processed_condition: 'host_alarm__on__host AND host__contains__instance' + extracted_relationships: { + host_alarm__on__host: EdgeDescription(...), + host__contains__instance: EdgeDescription(...), + } + """ + regex = r"(\w+)\s*\[\s*(\w+)\s*\]\s*(\w+)" + extracted_relationships = dict() + + def relation_str(matchobj): + source = matchobj.group(1) + label = matchobj.group(2) + target = matchobj.group(3) + relation_id = '%s__%s__%s' % (source, label, target) + extracted_relationships[relation_id] = EdgeDescription( + Edge(source, target, label, dict()), + entities[source], + entities[target]) + return relation_id + + processed_condition = re.sub(regex, relation_str, condition) + return extracted_relationships, processed_condition + + +def _build_scenarios(template, entities, relationships, schema): + name = template[TFields.METADATA][TFields.NAME] + + scenarios = [] + for index, scenario in enumerate(template[TFields.SCENARIOS]): + s_id = "%s-scenario%d" % (name, index) + condition = parse_condition(scenario.get(TFields.CONDITION)) + default_target = calculate_action_target(condition, + entities, + relationships) + + if not default_target: + raise ValidationError(135, 'scenario %d' % index, + scenario.get(TFields.CONDITION)) + + tmp_scenario = Scenario( + id=s_id, + version=schema.version(), + condition=None, + actions=_build_actions(schema, scenario, s_id, default_target), + subgraphs=_build_subgraphs(condition, entities, relationships), + entities=entities, + relationships=relationships) + scenarios.append(tmp_scenario) + return scenarios + + +def _build_actions(template_schema, scenario, scenario_id, default_target): + actions = [] + actions_def = scenario[TFields.ACTIONS] + for counter, action_def in enumerate(actions_def): + action_id = '%s-action%d' % (scenario_id, counter) + action_type, action_props = action_def.popitem() + action = template_schema.loaders.get(action_type).load( + action_id, + default_target, + action_props, + action_type + ) + actions.append(action) + return actions + + +def _build_subgraphs(condition, entities, relationships): + + def _extract_var(symbol_name): + if symbol_name in relationships: + edge_descriptor = relationships[symbol_name] + return edge_descriptor, RELATIONSHIP + elif symbol_name in entities: + vertex = entities[symbol_name] + return vertex, ENTITY + + return SubGraphBuilder.from_condition(condition, _extract_var) diff --git a/vitrage/evaluator/template_loading/v1/action_loader.py b/vitrage/evaluator/template_loading/v1/action_loader.py index 5b96a0d82..f6160d8bc 100644 --- a/vitrage/evaluator/template_loading/v1/action_loader.py +++ b/vitrage/evaluator/template_loading/v1/action_loader.py @@ -20,10 +20,10 @@ from vitrage.evaluator.template_fields import TemplateFields as TFields class BaseActionLoader(object): - def load(self, action_id, valid_target, action_def): + def load(self, action_id, default_target, action_def): action_dict = action_def[TFields.ACTION] action_type = action_dict[TFields.ACTION_TYPE] - targets = action_dict.get(TFields.ACTION_TARGET, valid_target) + targets = action_dict.get(TFields.ACTION_TARGET, default_target) return ActionSpecs(action_id, action_type, targets, self._get_properties(action_dict)) diff --git a/vitrage/evaluator/template_loading/v3/__init__.py b/vitrage/evaluator/template_loading/v3/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vitrage/evaluator/template_loading/v3/action_loader.py b/vitrage/evaluator/template_loading/v3/action_loader.py new file mode 100644 index 000000000..1b019997a --- /dev/null +++ b/vitrage/evaluator/template_loading/v3/action_loader.py @@ -0,0 +1,35 @@ +# Copyright 2019 - 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 vitrage.evaluator.template_data import ActionSpecs +from vitrage.evaluator.template_fields import TemplateFields as TField + + +class ActionLoader(object): + + def load(self, action_id, default_target, action_dict, action_type): + """V3 template action to ActionSpecs transformation + + :param action_id: Unique action identifier + :param default_target: Is taken from the condition, + it is used when the action doesn't define a target + :param action_dict: Action section taken from the template. + :param action_type: example: set_state/raise_alarm/etc.. + :rtype: ActionSpecs + """ + target = action_dict.pop(TField.TARGET, default_target[TField.TARGET]) + targets = {TField.TARGET: target} + if action_dict.get(TField.SOURCE): + targets[TField.SOURCE] = action_dict.pop(TField.SOURCE) + return ActionSpecs(action_id, action_type, targets, action_dict) diff --git a/vitrage/evaluator/template_schemas.py b/vitrage/evaluator/template_schemas.py index d35c2655f..e8e2a84cf 100644 --- a/vitrage/evaluator/template_schemas.py +++ b/vitrage/evaluator/template_schemas.py @@ -14,13 +14,21 @@ from oslo_log import log from vitrage.evaluator.actions.base import ActionType +from vitrage.evaluator import base from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_functions.v2.functions import get_attr from vitrage.evaluator.template_functions.v2.functions import GET_ATTR +from vitrage.evaluator.template_loading.template_loader import TemplateLoader +from vitrage.evaluator.template_loading.template_loader_v3 import\ + TemplateLoader as V3TemplateLoader from vitrage.evaluator.template_loading.v1.action_loader import ActionLoader from vitrage.evaluator.template_loading.v1.execute_mistral_loader import \ ExecuteMistralLoader +from vitrage.evaluator.template_loading.v3.action_loader import ActionLoader \ + as V3ActionLoader from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory +from vitrage.evaluator.template_validation.content.\ + template_content_validator_v3 import ContentValidator as V3ContentValidator from vitrage.evaluator.template_validation.content.v1.\ add_causal_relationship_validator import AddCausalRelationshipValidator from vitrage.evaluator.template_validation.content.v1.definitions_validator \ @@ -43,6 +51,8 @@ from vitrage.evaluator.template_validation.content.v2.\ V2ExecuteMistralValidator from vitrage.evaluator.template_validation.content.v2.metadata_validator \ import MetadataValidator as V2MetadataValidator +from vitrage.evaluator.template_validation.template_syntax_validator_v3 import\ + SyntaxValidator as V3SyntaxValidator LOG = log.getLogger(__name__) @@ -61,6 +71,7 @@ class TemplateSchema1(object): } self.loaders = { + base.TEMPLATE_LOADER: TemplateLoader(), ActionType.ADD_CAUSAL_RELATIONSHIP: ActionLoader(), ActionType.EXECUTE_MISTRAL: ExecuteMistralLoader(), ActionType.MARK_DOWN: ActionLoader(), @@ -88,6 +99,30 @@ class TemplateSchema2(TemplateSchema1): return '2' +class TemplateSchema3(object): + + def __init__(self): + self.validators = { + TemplateFields.METADATA: V2MetadataValidator, + base.SYNTAX: V3SyntaxValidator, + base.CONTENT: V3ContentValidator, + } + self.loaders = { + base.TEMPLATE_LOADER: V3TemplateLoader(), + ActionType.ADD_CAUSAL_RELATIONSHIP: V3ActionLoader(), + ActionType.EXECUTE_MISTRAL: V3ActionLoader(), + ActionType.MARK_DOWN: V3ActionLoader(), + ActionType.RAISE_ALARM: V3ActionLoader(), + ActionType.SET_STATE: V3ActionLoader(), + } + + self.functions = {GET_ATTR: get_attr} + + def version(self): + return '3' + + def init_template_schemas(): TemplateSchemaFactory.register_template_schema('1', TemplateSchema1()) TemplateSchemaFactory.register_template_schema('2', TemplateSchema2()) + TemplateSchemaFactory.register_template_schema('3', TemplateSchema3()) diff --git a/vitrage/evaluator/template_validation/__init__.py b/vitrage/evaluator/template_validation/__init__.py index fd7685058..3b8c10807 100644 --- a/vitrage/evaluator/template_validation/__init__.py +++ b/vitrage/evaluator/template_validation/__init__.py @@ -12,8 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. from oslo_log import log +from vitrage.evaluator.base import CONTENT +from vitrage.evaluator.base import SYNTAX +from voluptuous import Error as VoluptuousError from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_validation import base from vitrage.evaluator.template_validation.content.base import \ get_template_schema from vitrage.evaluator.template_validation.content.template_content_validator \ @@ -28,6 +32,23 @@ LOG = log.getLogger(__name__) def validate_template(template, def_templates): + result, template_schema = get_template_schema(template) + if not result.is_valid_config: + return result + if template_schema.version() < '3': + return _validate_template_v1_v2(template, def_templates) + + try: + template_schema.validators[SYNTAX].validate(template) + template_schema.validators[CONTENT].validate(template) + except base.ValidationError as e: + return base.get_custom_fault_result(e.code, e.details) + except VoluptuousError as e: + return base.get_custom_fault_result(base.get_status_code(e), str(e)) + return base.get_correct_result() + + +def _validate_template_v1_v2(template, def_templates): result = syntax_validation(template) if not result.is_valid_config: LOG.error('Unable to load template, syntax error: %s' % result.comment) diff --git a/vitrage/evaluator/template_validation/base.py b/vitrage/evaluator/template_validation/base.py index 4f46b06a1..9c2e1d04e 100644 --- a/vitrage/evaluator/template_validation/base.py +++ b/vitrage/evaluator/template_validation/base.py @@ -13,12 +13,21 @@ # under the License. from collections import namedtuple from vitrage.evaluator.template_validation.status_messages import status_msgs +RESULT_DESCRIPTION = 'Template syntax validation' +EXCEPTION = 'exception' Result = namedtuple('Result', ['description', 'is_valid_config', 'status_code', 'comment']) -def get_correct_result(description): +class ValidationError(Exception): + def __init__(self, code, *args): + self.code = code + self.details = '' + self.details = ','.join(str(arg) for arg in args) + + +def get_correct_result(description=RESULT_DESCRIPTION): return Result(description, True, 0, status_msgs[0]) @@ -30,3 +39,15 @@ def get_fault_result(description, code, msg=None): if msg: return Result(description, False, code, msg) return Result(description, False, code, status_msgs[code]) + + +def get_custom_fault_result(code, msg): + return Result('Template validation', False, code, + status_msgs[code] + ' ' + msg) + + +def get_status_code(voluptuous_error): + prefix = str(voluptuous_error).split(' ')[0].strip() + if prefix.isdigit(): + return int(prefix) + return 4 diff --git a/vitrage/evaluator/template_validation/content/template_content_validator_v3.py b/vitrage/evaluator/template_validation/content/template_content_validator_v3.py new file mode 100644 index 000000000..bc66aaa0c --- /dev/null +++ b/vitrage/evaluator/template_validation/content/template_content_validator_v3.py @@ -0,0 +1,135 @@ +# Copyright 2019 - 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 re + +from oslo_log import log +from sympy import Not +from sympy import Symbol + +from vitrage.evaluator.base import get_template_schema +from vitrage.evaluator import condition as dnf + +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_loading.template_loader_v3 import \ + TemplateLoader as V3TemplateLoader +from vitrage.evaluator.template_validation.base import ValidationError + +LOG = log.getLogger(__name__) + +RELATION = 'relationship' + + +class ContentValidator(object): + + @staticmethod + def validate(template): + _validate_entities_regex(template) + _validate_conditions(template) + + # As part of validation, when it is finished, + # we try to load the template, as some validations can only be + # executed at loading phase + schema = get_template_schema(template) + V3TemplateLoader().load(schema, template) + + +def _validate_entities_regex(template): + for entity in template[TemplateFields.ENTITIES].values(): + for key, value in entity.items(): + if key.lower().endswith(TemplateFields.REGEX): + try: + re.compile(value) + except Exception: + raise ValidationError(47, key, value) + + +def _validate_conditions(template): + """Validate conditions + + 'alarm_1 [on] host AND host [contains] instance AND alarm_2 [on] instance + AND host' + In this condition , replace all occurrences of 'source [label] target' + so to create : + 'relationship AND relationship AND relationship AND host' + """ + for scenario in template[TemplateFields.SCENARIOS]: + condition = scenario[TemplateFields.CONDITION] + _validate_condition_entity_ids(template, condition) + _validate_not_condition(condition) + + +def _validate_condition_entity_ids(template, condition): + curr_str = ' ' + condition + ' ' + + # Remove all [label] occurrences + edge_labels_re = r'\s+\[\s*\w+\s*\]\s+' + curr_str = re.sub(edge_labels_re, ' ', curr_str) + if '[' in curr_str or ']' in curr_str: + raise ValidationError(85, condition) + + # Remove all operator occurrences + operators_re = r'\b(AND|OR|NOT|and|or|not)\b' + curr_str = re.sub(operators_re, '', curr_str) + + # Remove all entity occurrences + for entity_id in template[TemplateFields.ENTITIES].keys(): + entity_id_regex = r'\b' + entity_id + r'\b' + curr_str = re.sub(entity_id_regex, ' ', curr_str) + + # Remove all parentheses occurrences + curr_str = curr_str.replace('(', '') + curr_str = curr_str.replace(')', '') + + # Remaining string should be empty + if curr_str.strip(): + raise ValidationError(10200, condition, curr_str) + + +def _validate_not_condition(condition): + """Not operator validation + + 1. Not operator can appear only on relationships. + 2. There must be at least one positive term + """ + regex = r"(\w+)\s*\[\s*(\w+)\s*\]\s*(\w+)" + preprocessed_condition = re.sub(regex, RELATION, condition) + + try: + dnf_condition = dnf.convert_to_dnf_format(preprocessed_condition) + _validate_not_condition_relationships_recursive(dnf_condition) + dnf_condition = dnf.parse_condition(preprocessed_condition) + _validate_positive_term_in_condition(dnf_condition) + except ValidationError as e: + raise ValidationError(e.code, e.details, condition) + except Exception as e: + raise ValidationError(85, condition, e) + + +def _validate_not_condition_relationships_recursive(dnf_result): + if isinstance(dnf_result, Not): + for arg in dnf_result.args: + if isinstance(arg, Symbol) and not str(arg).startswith(RELATION): + raise ValidationError(86, arg) + else: + _validate_not_condition_relationships_recursive(arg) + return + + for arg in dnf_result.args: + if not isinstance(arg, Symbol): + _validate_not_condition_relationships_recursive(arg) + + +def _validate_positive_term_in_condition(dnf_condition): + if not dnf.is_condition_include_positive_clause(dnf_condition): + raise ValidationError(134) diff --git a/vitrage/evaluator/template_validation/status_messages.py b/vitrage/evaluator/template_validation/status_messages.py index c510dcae2..7f0a7c09b 100644 --- a/vitrage/evaluator/template_validation/status_messages.py +++ b/vitrage/evaluator/template_validation/status_messages.py @@ -54,8 +54,8 @@ status_msgs = { 80: 'scenarios is a mandatory section.', 81: 'At least one scenario must be defined.', 82: 'scenario field is required.', - 83: 'Entity definition must contain condition field.', - 84: 'Entity definition must contain actions field.', + 83: 'Scenario definition must contain condition field.', + 84: 'Scenario definition must contain actions field.', 85: 'Failed to convert condition.', 86: 'Not operator can be used only on relationships.', @@ -90,7 +90,7 @@ status_msgs = { 136: 'Input parameters for the Mistral workflow in execute_mistral action ' 'must be placed under an \'input\' block ', 137: 'Functions are supported only from version 2', - 138: 'Warning: only open or close parenthesis exists. Did you try to use ' + 138: 'Only open or close parenthesis exists. Did you try to use ' 'a function?', # def_templates status messages 140-159 @@ -99,4 +99,14 @@ status_msgs = { 142: 'Trying to include a template that does not exist', 143: 'A template definition file cannot contain \'includes\' or ' '\'scenarios\' blocks', + + 10100: 'Action must contain a \'target\' property', + 10101: 'Action \'target\' must match an entity id', + 10102: 'Action must contain a \'source\' property', + 10103: 'Action \'source\' must match an entity id', + 10104: 'raise_alarm action must contain an \'alarm_name\' property', + 10105: 'execute_mistral action must contain \'workflow\' property', + 10107: 'The property \'causing_alarm\' in raise_alarm action must match an' + ' entity id', + 10200: 'Condition contains an unknown entity id', } diff --git a/vitrage/evaluator/template_validation/template_syntax_validator.py b/vitrage/evaluator/template_validation/template_syntax_validator.py index bc958986f..391ed1ade 100644 --- a/vitrage/evaluator/template_validation/template_syntax_validator.py +++ b/vitrage/evaluator/template_validation/template_syntax_validator.py @@ -28,6 +28,7 @@ from vitrage.evaluator.actions.base import action_types from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_validation.base import get_correct_result from vitrage.evaluator.template_validation.base import get_fault_result +from vitrage.evaluator.template_validation.base import get_status_code from vitrage.evaluator.template_validation.status_messages import status_msgs LOG = log.getLogger(__name__) @@ -145,7 +146,7 @@ def _validate_name_schema(schema, name): schema(name) except Error as e: - status_code = _get_status_code(e) + status_code = get_status_code(e) if status_code: msg = status_msgs[status_code] else: @@ -319,28 +320,14 @@ def _validate_dict_schema(schema, value): try: schema(value) except Error as e: - - status_code = _get_status_code(e) - if status_code: - msg = status_msgs[status_code] - else: - # General syntax error - status_code = 4 - msg = status_msgs[4] + str(e) - + status_code = get_status_code(e) + msg = status_msgs[status_code] + str(e) LOG.error('%s status code: %s' % (msg, status_code)) return get_fault_result(RESULT_DESCRIPTION, status_code, msg) return get_correct_result(RESULT_DESCRIPTION) -def _get_status_code(e): - prefix = str(e).split(' ')[0].strip() - if prefix.isdigit(): - return int(prefix) - return None - - def _validate_template_id_value(msg=None): def f(v): if re.match("_*[a-zA-Z]+\\w*", str(v)): diff --git a/vitrage/evaluator/template_validation/template_syntax_validator_v3.py b/vitrage/evaluator/template_validation/template_syntax_validator_v3.py new file mode 100644 index 000000000..f0eba6a65 --- /dev/null +++ b/vitrage/evaluator/template_validation/template_syntax_validator_v3.py @@ -0,0 +1,137 @@ +# Copyright 2019 - 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 re +import six + +from oslo_log import log +from voluptuous import Any +from voluptuous import In +from voluptuous import Invalid +from voluptuous import Optional +from voluptuous import Required +from voluptuous import Schema + +from vitrage.common.constants import TemplateTypes +from vitrage.evaluator.actions.base import ActionType +from vitrage.evaluator.actions.recipes.execute_mistral import INPUT +from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW +from vitrage.evaluator.base import is_function +from vitrage.evaluator.template_fields import TemplateFields as TF +from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory + +LOG = log.getLogger(__name__) + + +any_str = Any(str, six.text_type) + + +class SyntaxValidator(object): + + @staticmethod + def validate(template): + Schema({ + Required(TF.ENTITIES, msg=10000): _entities_schema(), + Required(TF.METADATA, msg=62): _metadata_schema(), + Required(TF.SCENARIOS, msg=80): _scenarios_schema(template), + })(template) + + +def _entities_schema(): + return Schema({ + any_str: Schema({ + any_str: any_str, + })}) + + +def _metadata_schema(): + return Schema({ + Required(TF.VERSION, msg=63): In( + TemplateSchemaFactory.supported_versions()), + Required(TF.NAME, msg=60): any_str, + TF.DESCRIPTION: any_str, + Required(TF.TYPE, msg=64): In(TemplateTypes.types(), msg=64), + }) + + +def _scenarios_schema(template): + + return Schema([ + Schema({ + Required(TF.ACTIONS, msg=84): Schema([Any( + _raise_alarm_schema(template), + _set_state_schema(template), + _add_causal_relationship_schema(template), + _mark_down_schema(template), + _execute_mistral_schema(), + )]), + Required(TF.CONDITION, msg=83): any_str, + })]) + + +def _raise_alarm_schema(template): + return Schema({ + Optional(ActionType.RAISE_ALARM): Schema({ + Required(TF.SEVERITY, msg=126): any_str, + Required(TF.TARGET, msg=10100): + In(template.get(TF.ENTITIES, {}).keys(), msg=10101), + Required(TF.ALARM_NAME, msg=10104): any_str, + Optional(TF.CAUSING_ALARM): + In(template.get(TF.ENTITIES, {}).keys(), msg=10107), + })}) + + +def _set_state_schema(template): + return Schema({ + Optional(ActionType.SET_STATE): Schema({ + Required(TF.STATE, msg=128): any_str, + Required(TF.TARGET, msg=10100): + In(template.get(TF.ENTITIES, {}).keys(), msg=10101), + })}) + + +def _add_causal_relationship_schema(template): + return Schema({ + Optional(ActionType.ADD_CAUSAL_RELATIONSHIP): Schema({ + Required(TF.SOURCE, msg=10102): + In(template.get(TF.ENTITIES, {}).keys(), msg=10103), + Required(TF.TARGET, msg=10100): + In(template.get(TF.ENTITIES, {}).keys(), msg=10101), + })}) + + +def _mark_down_schema(template): + return Schema({ + Optional(ActionType.MARK_DOWN): Schema({ + Required(TF.TARGET, msg=10100): + In(template.get(TF.ENTITIES, {}).keys(), msg=10101), + })}) + + +def _execute_mistral_schema(): + + return Schema({ + Optional(ActionType.EXECUTE_MISTRAL): Schema({ + Required(WORKFLOW, msg=10105): any_str, + Optional(INPUT): Schema({ + any_str: IsProperFunction()} + )})}) + + +class IsProperFunction(object): + """If Value contains parentheses, check it is a proper function call""" + + def __call__(self, v): + if re.findall('[(),]', v) and not is_function(v): + raise Invalid(138) + return v diff --git a/vitrage/graph/filter.py b/vitrage/graph/filter.py index edcc6de1a..a3ac0da1a 100644 --- a/vitrage/graph/filter.py +++ b/vitrage/graph/filter.py @@ -12,7 +12,6 @@ # License for the specific language governing permissions and limitations # under the License. -from vitrage.evaluator.template_fields import TemplateFields as Fields from vitrage.graph.utils import check_property_with_regex @@ -34,8 +33,9 @@ def check_filter(data, attr_filter, *args): if not isinstance(content, list): content = [content] if data.get(key) not in content: - if key.lower().endswith(Fields.REGEX): - new_key = key[:-len(Fields.REGEX)] + # import of .regex constant removed, as it was circular + if key.lower().endswith('.regex'): + new_key = key[:-len('.regex')] if not check_property_with_regex(new_key, content[0], data): return False diff --git a/vitrage/tests/mocks/graph_generator.py b/vitrage/tests/mocks/graph_generator.py index 86fa2e7df..097ae4782 100644 --- a/vitrage/tests/mocks/graph_generator.py +++ b/vitrage/tests/mocks/graph_generator.py @@ -17,6 +17,7 @@ import copy from vitrage.common.constants import EdgeProperties from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources import NOVA_HOST_DATASOURCE from vitrage.graph import Direction from vitrage.graph.driver.networkx_graph import NXGraph from vitrage.graph import Edge @@ -131,6 +132,10 @@ class GraphGenerator(object): for i in range(n): v = self._file_to_vertex(neighbor_props_file, i) v[VProps.NAME] = v[VProps.NAME] + "-" + source_v[VProps.NAME] + + if v.get(VProps.VITRAGE_TYPE) == NOVA_HOST_DATASOURCE: + v[VProps.ID] = v.get(VProps.NAME) + created_vertices.append(v) g.add_vertex(v) if direction == Direction.OUT: diff --git a/vitrage/tests/resources/templates/v3_templates/valid_actions.yaml b/vitrage/tests/resources/templates/v3_templates/valid_actions.yaml new file mode 100644 index 000000000..6a96c4b16 --- /dev/null +++ b/vitrage/tests/resources/templates/v3_templates/valid_actions.yaml @@ -0,0 +1,63 @@ +metadata: + version: 3 + name: valid actions + description: zabbix alarm for network interface and ssh affects host instances + type: standard +entities: + host_network_alarm: + type: zabbix + rawtext: host network interface is down + host_ssh_alarm: + type: zabbix + rawtext: host ssh is down + instance: + type: nova.instance + host: + type: nova.host + foo: + name.regex: kuku +scenarios: + - condition: host_ssh_alarm [ on ] host + actions: + - set_state: + state: ERROR + target: host + - raise_alarm: + target: host + alarm_name: ddd + severity: WARNING + - mark_down: + target: host + - execute_mistral: + workflow: wf_1234 + input: + farewell: get_attr(host, name) bla bla + - condition: host_network_alarm [ on ] host AND host_ssh_alarm [ on ] host + actions: + - add_causal_relationship: + source: host_network_alarm + target: host_ssh_alarm + - condition: host_ssh_alarm [ on ] host AND host [ contains ] instance + actions: + - raise_alarm: + target: instance + alarm_name: instance is down + severity: WARNING + causing_alarm: host_ssh_alarm + - set_state: + state: SUBOPTIMAL + target: instance + - condition: host AND NOT host_ssh_alarm [ on ] host or host [ contains ] instance + actions: + - mark_down: + target: host + - condition: host + actions: + - mark_down: + target: host + - condition: host AND NOT (host_ssh_alarm [ on ] host or host [ contains ] instance) + actions: + - mark_down: + target: host + + diff --git a/vitrage/tests/resources/templates/v3_templates/valid_conditions.yaml b/vitrage/tests/resources/templates/v3_templates/valid_conditions.yaml new file mode 100644 index 000000000..8e4ebdbd1 --- /dev/null +++ b/vitrage/tests/resources/templates/v3_templates/valid_conditions.yaml @@ -0,0 +1,58 @@ +metadata: + version: 3 + name: aaa + description: aaa + type: standard +entities: + instance: + type: nova.instance + host: + type: nova.host + host_ssh_alarm: + name: ssh alarm + type: zabbix +scenarios: + - condition: host + actions: + - mark_down: + target: host + - condition: host [ contains] instance AND host [contains] instance + actions: + - mark_down: + target: host + - condition: host AND NOT host [ contains ] instance + actions: + - mark_down: + target: host + - condition: host and not host [ contains ] instance or host + actions: + - mark_down: + target: host + - condition: host OR host or host and instance AND instance + actions: + - mark_down: + target: host + - condition: (host and NOT host [ contains ] instance) or host [ contains ] instance + actions: + - mark_down: + target: host + - condition: (host [contains] instance) + actions: + - mark_down: + target: host + - condition: (host [contains] instance AND host [contains] instance) + actions: + - mark_down: + target: host + - condition: ( host [contains] instance OR host [contains] instance ) + actions: + - mark_down: + target: host + - condition: ( host OR host [contains] instance ) AND (instance OR host [contains] instance) + actions: + - mark_down: + target: host + - condition: host AND NOT host [contains] instance + actions: + - mark_down: + target: host \ No newline at end of file diff --git a/vitrage/tests/unit/evaluator/__init__.py b/vitrage/tests/unit/evaluator/__init__.py index e69de29bb..350114cc1 100644 --- a/vitrage/tests/unit/evaluator/__init__.py +++ b/vitrage/tests/unit/evaluator/__init__.py @@ -0,0 +1,21 @@ +# Copyright 2019 - 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 vitrage.evaluator.base import get_template_schema +from vitrage.evaluator.base import TEMPLATE_LOADER + + +def get_template_data(template, def_templates=None): + schema = get_template_schema(template) + template_loader = schema.loaders[TEMPLATE_LOADER] + return template_loader.load(schema, template, def_templates) diff --git a/vitrage/tests/unit/evaluator/template_loading/test_template_loading_v3.py b/vitrage/tests/unit/evaluator/template_loading/test_template_loading_v3.py new file mode 100644 index 000000000..217397e73 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_loading/test_template_loading_v3.py @@ -0,0 +1,450 @@ +# Copyright 2019 - 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_config import cfg +from vitrage.evaluator.template_data import ActionSpecs +from vitrage.evaluator.template_data import EdgeDescription +from vitrage.evaluator.template_data import Scenario +from vitrage.graph.driver.networkx_graph import NXGraph +from vitrage.graph import Edge +from vitrage.graph import Vertex +from vitrage.tests.base import BaseTest +from vitrage.tests.functional.test_configuration import TestConfiguration +from vitrage.tests.mocks.utils import get_resources_dir +from vitrage.tests.unit.evaluator import get_template_data +from vitrage.utils import file as file_utils + + +class TemplateLoaderV3Test(BaseTest, TestConfiguration): + + expected_entities = { + 'host_ssh_alarm': Vertex('host_ssh_alarm', { + 'rawtext': 'host ssh is down', 'vitrage_type': 'zabbix'}), + 'host': Vertex('host', {'vitrage_type': 'nova.host'}), + 'foo': Vertex('foo', {'name.regex': 'kuku'}), + 'host_network_alarm': Vertex('host_network_alarm', { + 'rawtext': 'host network interface is down', + 'vitrage_type': 'zabbix', + }), + 'instance': Vertex('instance', {'vitrage_type': 'nova.instance'}), + } + expected_relationships = { + 'host_ssh_alarm__on__host': EdgeDescription( + Edge('host_ssh_alarm', 'host', 'on', {}), + Vertex('host_ssh_alarm', { + 'rawtext': 'host ssh is down', 'vitrage_type': 'zabbix' + }), + Vertex('host', {'vitrage_type': 'nova.host'})), + 'host__contains__instance': EdgeDescription( + Edge('host', 'instance', 'contains', {}), + Vertex('host', {'vitrage_type': 'nova.host'}), + Vertex('instance', {'vitrage_type': 'nova.instance'})), + 'host_network_alarm__on__host': EdgeDescription( + Edge('host_network_alarm', 'host', 'on', {}), + Vertex('host_network_alarm', { + 'rawtext': 'host network interface is down', + 'vitrage_type': 'zabbix' + }), + Vertex('host', {'vitrage_type': 'nova.host'})), + } + + @classmethod + def setUpClass(cls): + super(TemplateLoaderV3Test, cls).setUpClass() + cls.conf = cfg.ConfigOpts() + cls.add_db(cls.conf) + + def _load_scenarios(self, file=None, content=None): + if file and not content: + content = self._get_yaml(file) + return get_template_data(content).scenarios + + def _assert_scenario_equal(self, expected, observed): + + # Basic + self.assertEqual(expected.id, observed.id) + self.assertEqual(expected.version, observed.version) + self.assertEqual(expected.condition, observed.condition) # is None + + # Actions + self.assertEqual(len(expected.actions), len(observed.actions), + 'actions count') + for j in range(len(expected.actions)): + expected_action = expected.actions[j] + observed_action = observed.actions[j] + self.assertEqual(expected_action.id, observed_action.id) + self.assertEqual(expected_action.type, observed_action.type) + self.assertEqual(expected_action.properties, + observed_action.properties) + if expected_action.type == 'execute_mistral': + continue + self.assertEqual(expected_action.targets, observed_action.targets) + + # Subgraphs + self.assertEqual(len(expected.subgraphs), len(observed.subgraphs), + 'subgraphs count') + for j in range(len(expected.subgraphs)): + expected_subgraph = expected.subgraphs[j] + observed_subgraph = observed.subgraphs[j] + self.assert_graph_equal(expected_subgraph, observed_subgraph) + + # Entities + self.assert_dict_equal(expected.entities, observed.entities, + 'entities comparison') + self.assert_dict_equal(expected.relationships, observed.relationships, + 'relationships comparison') + + @staticmethod + def _get_yaml(filename): + path = '%s/templates/v3_templates/%s' % (get_resources_dir(), filename) + return file_utils.load_yaml_file(path) + + def test_scenarios(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + self.assertEqual(6, len(observed_scenarios), 'scenarios count') + + def test_scenario_0(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario0', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario0-action0', + 'set_state', + {'target': 'host'}, + {'state': 'ERROR'}), + ActionSpecs( + 'valid actions-scenario0-action1', + 'raise_alarm', + {'target': 'host'}, + {'severity': 'WARNING', 'alarm_name': 'ddd'}), + ActionSpecs( + 'valid actions-scenario0-action2', + 'mark_down', + {'target': 'host'}, + {}), + ActionSpecs( + 'valid actions-scenario0-action3', + 'execute_mistral', + {'target': 'host'}, + {'input': {'farewell': 'get_attr(host, name) bla bla'}, + 'workflow': 'wf_1234'}), + ], + [ + NXGraph( + vertices=[ + Vertex('host_ssh_alarm', + { + 'rawtext': 'host ssh is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }) + ], + edges=[ + Edge('host_ssh_alarm', 'host', 'on', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }) + ]) + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[0]) + + def test_scenario_1(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario1', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario1-action0', + 'add_causal_relationship', + { + 'target': 'host_ssh_alarm', + 'source': 'host_network_alarm', + }, + {}), + ], + [ + NXGraph( + vertices=[ + Vertex('host_ssh_alarm', + { + 'rawtext': 'host ssh is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('host_network_alarm', + { + 'rawtext': 'host network interface is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }) + ], + edges=[ + Edge('host_ssh_alarm', 'host', 'on', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }), + Edge('host_network_alarm', 'host', 'on', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }) + ]) + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[1]) + + def test_scenario_2(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario2', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario2-action0', + 'raise_alarm', + {'target': 'instance'}, + { + 'severity': 'WARNING', + 'alarm_name': 'instance is down', + 'causing_alarm': 'host_ssh_alarm', + }), + ActionSpecs( + 'valid actions-scenario2-action1', + 'set_state', + {'target': 'instance'}, + {'state': 'SUBOPTIMAL'}), + ], + [ + NXGraph( + vertices=[ + Vertex('host_ssh_alarm', + { + 'rawtext': 'host ssh is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('instance', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.instance', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }), + ], + edges=[ + Edge('host_ssh_alarm', 'host', 'on', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }), + Edge('host', 'instance', 'contains', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }) + ]) + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[2]) + + def test_scenario_3(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario3', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario3-action0', + 'mark_down', + {'target': 'host'}, + {}), + ], + [ + NXGraph( + vertices=[ + Vertex('instance', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.instance', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }), + ], + edges=[ + Edge('host', 'instance', 'contains', + { + 'vitrage_is_deleted': False, + 'negative_condition': False + }) + ]), + NXGraph( + vertices=[ + Vertex('host_ssh_alarm', + { + 'rawtext': 'host ssh is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }), + ], + edges=[ + Edge('host_ssh_alarm', 'host', 'on', + { + 'vitrage_is_deleted': True, + 'negative_condition': True, + }), + ]), + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[3]) + + def test_scenario_4(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario4', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario4-action0', + 'mark_down', + {'target': 'host'}, + {}), + ], + [ + NXGraph( + vertices=[ + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }), + ]), + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[4]) + + def test_scenario_5(self): + observed_scenarios = self._load_scenarios('valid_actions.yaml') + expected_scenario = Scenario( + 'valid actions-scenario5', + '3', + None, + [ + ActionSpecs( + 'valid actions-scenario5-action0', + 'mark_down', + {'target': 'host'}, + {}), + ], + [ + NXGraph( + vertices=[ + Vertex('host_ssh_alarm', + { + 'rawtext': 'host ssh is down', + 'vitrage_is_placeholder': False, + 'vitrage_type': 'zabbix', + 'vitrage_is_deleted': False, + }), + Vertex('instance', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.instance', + 'vitrage_is_deleted': False, + }), + Vertex('host', + { + 'vitrage_is_placeholder': False, + 'vitrage_type': 'nova.host', + 'vitrage_is_deleted': False, + }), + ], + edges=[ + Edge('host_ssh_alarm', 'host', 'on', + { + 'vitrage_is_deleted': True, + 'negative_condition': True + }), + Edge('host', 'instance', 'contains', + { + 'vitrage_is_deleted': True, + 'negative_condition': True + }) + ] + ), + ], + TemplateLoaderV3Test.expected_entities, + TemplateLoaderV3Test.expected_relationships) + self._assert_scenario_equal( + expected_scenario, + observed_scenarios[5]) diff --git a/vitrage/tests/unit/evaluator/template_loading/v2/test_template_loader.py b/vitrage/tests/unit/evaluator/template_loading/v2/test_template_loader.py index 7d8ba7044..33f4658da 100644 --- a/vitrage/tests/unit/evaluator/template_loading/v2/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/template_loading/v2/test_template_loader.py @@ -27,9 +27,9 @@ # under the License. from vitrage.common.constants import TemplateTypes -from vitrage.evaluator.template_loading.template_loader import TemplateLoader from vitrage.tests import base from vitrage.tests.mocks import utils +from vitrage.tests.unit.evaluator import get_template_data from vitrage.utils import file as file_utils @@ -42,7 +42,7 @@ class TemplateLoaderTest(base.BaseTest): (utils.get_resources_dir(), self.STANDARD_TEMPLATE) template_definition = file_utils.load_yaml_file(template_path, True) - template_data = TemplateLoader().load(template_definition) + template_data = get_template_data(template_definition) self.assertIsNotNone(template_data) template_type = template_data.template_type diff --git a/vitrage/tests/unit/evaluator/template_validation/test_template_validator_v3.py b/vitrage/tests/unit/evaluator/template_validation/test_template_validator_v3.py new file mode 100644 index 000000000..29200afad --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/test_template_validator_v3.py @@ -0,0 +1,346 @@ +# Copyright 2019 - 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 json +from oslo_config import cfg + +from vitrage.api_handler.apis.template import TemplateApis +from vitrage.evaluator.actions.recipes import execute_mistral +from vitrage.evaluator.template_fields import TemplateFields as TF +from vitrage.tests import base +from vitrage.tests.functional.test_configuration import TestConfiguration +from vitrage.tests.mocks.utils import get_resources_dir +from vitrage.utils import file as file_utils + + +class TemplateValidatorV3Test(base.BaseTest, TestConfiguration): + + @classmethod + def setUpClass(cls): + super(TemplateValidatorV3Test, cls).setUpClass() + cls.conf = cfg.ConfigOpts() + cls.add_db(cls.conf) + cls.template_apis = TemplateApis(db=cls._db) + + def _test_validation(self, file=None, content=None, expected_code=0): + if file and not content: + content = self._get_yaml(file) + self._call_validate_api('/tmp/tmp', content, expected_code) + + @staticmethod + def _get_yaml(filename): + path = '%s/templates/v3_templates/%s' % (get_resources_dir(), filename) + return file_utils.load_yaml_file(path) + + def _call_validate_api(self, path, content, expected_error_code): + templates = [[ + path, + content, + ]] + results = self.template_apis.validate_template(None, templates, None) + result = json.loads(results)['results'][0] + self.assertEqual(expected_error_code, result['status code'], + message='GOT ' + result['message']) + + def test_actions(self): + template = self._get_yaml('valid_actions.yaml') + self._test_validation(content=template, expected_code=0) + + del template[TF.SCENARIOS][0][TF.CONDITION] + self._test_validation(content=template, expected_code=83) + + del template[TF.SCENARIOS][0] + del template[TF.SCENARIOS][0][TF.ACTIONS] + self._test_validation(content=template, expected_code=84) + + def test_set_state(self): + template = self._get_yaml('valid_actions.yaml') + + valid_action = { + 'set_state': { + TF.TARGET: 'host', + TF.STATE: 'BAD', + } + } + template[TF.SCENARIOS][0][TF.ACTIONS].append(valid_action) + self._test_validation(content=template, expected_code=0) + + valid_action['set_state'] = { + TF.TARGET: 'host_incorrect_key', + TF.STATE: 'BAD', + } + self._test_validation(content=template, expected_code=10101) + + valid_action['set_state'] = { + TF.STATE: 'BAD', + } + self._test_validation(content=template, expected_code=10100) + + valid_action['set_state'] = { + TF.TARGET: 'host', + TF.STATE: 'BAD', + 'kuku': 'kuku', + } + self._test_validation(content=template, expected_code=4) + + valid_action['set_state'] = { + TF.TARGET: 'host', + } + self._test_validation(content=template, expected_code=128) + + def test_mark_down(self): + template = self._get_yaml('valid_actions.yaml') + + valid_action = { + 'mark_down': { + TF.TARGET: 'host', + } + } + template[TF.SCENARIOS][0][TF.ACTIONS].append(valid_action) + self._test_validation(content=template, expected_code=0) + + valid_action['mark_down'] = { + TF.TARGET: 'host_incorrect_key', + } + self._test_validation(content=template, expected_code=10101) + + valid_action['mark_down'] = {} + self._test_validation(content=template, expected_code=10100) + + valid_action['mark_down'] = { + TF.TARGET: 'host', + 'kuku': 'kuku', + } + self._test_validation(content=template, expected_code=4) + + def test_raise_alarm(self): + self._test_validation(file='valid_actions.yaml', expected_code=0) + template = self._get_yaml('valid_actions.yaml') + + valid_action = { + 'raise_alarm': { + TF.TARGET: 'host', + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + } + } + template[TF.SCENARIOS][0][TF.ACTIONS].append(valid_action) + self._test_validation(content=template, expected_code=0) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host_incorrect_key', + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + } + self._test_validation(content=template, expected_code=10101) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host', + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + 'kuku': 'kuku', + } + self._test_validation(content=template, expected_code=4) + + valid_action['raise_alarm'] = { + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + } + self._test_validation(content=template, expected_code=10100) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host', + TF.SEVERITY: 'BAD', + } + self._test_validation(content=template, expected_code=10104) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host', + TF.ALARM_NAME: 'BAD', + } + self._test_validation(content=template, expected_code=126) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host', + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + TF.CAUSING_ALARM: 'host_ssh_alarm' + } + self._test_validation(content=template, expected_code=0) + + valid_action['raise_alarm'] = { + TF.TARGET: 'host', + TF.ALARM_NAME: 'BAD', + TF.SEVERITY: 'BAD', + TF.CAUSING_ALARM: 'host_ssh_alarm_incorrect_key' + } + self._test_validation(content=template, expected_code=10107) + + def test_add_causal_relationship(self): + self._test_validation(file='valid_actions.yaml', expected_code=0) + template = self._get_yaml('valid_actions.yaml') + + valid_action = { + 'add_causal_relationship': { + TF.TARGET: 'host_ssh_alarm', + TF.SOURCE: 'host_network_alarm', + } + } + template[TF.SCENARIOS][0][TF.ACTIONS].append(valid_action) + self._test_validation(content=template, expected_code=0) + + valid_action['add_causal_relationship'] = { + TF.TARGET: 'host_ssh_alarm_incorrect_key', + TF.SOURCE: 'host_network_alarm', + } + self._test_validation(content=template, expected_code=10101) + + valid_action['add_causal_relationship'] = { + TF.TARGET: 'host_ssh_alarm', + TF.SOURCE: 'host_network_alarm_incorrect_key', + } + self._test_validation(content=template, expected_code=10103) + + valid_action['add_causal_relationship'] = { + TF.SOURCE: 'host_network_alarm', + } + self._test_validation(content=template, expected_code=10100) + + valid_action['add_causal_relationship'] = { + TF.TARGET: 'host_ssh_alarm', + } + self._test_validation(content=template, expected_code=10102) + + def test_execute_mistral(self): + template = self._get_yaml('valid_actions.yaml') + + valid_action = { + 'execute_mistral': { + execute_mistral.WORKFLOW: 'kuku', + execute_mistral.INPUT: {}, + } + } + + template[TF.SCENARIOS][0][TF.ACTIONS].append(valid_action) + self._test_validation(content=template, expected_code=0) + + valid_action['execute_mistral'] = { + execute_mistral.WORKFLOW: {}, + execute_mistral.INPUT: {}, + } + self._test_validation(content=template, expected_code=4) + + valid_action['execute_mistral'] = {execute_mistral.INPUT: {}} + self._test_validation(content=template, expected_code=10105) + + valid_action['execute_mistral'] = {execute_mistral.WORKFLOW: 'kuku'} + self._test_validation(content=template, expected_code=0) + + valid_action['execute_mistral'] = { + execute_mistral.WORKFLOW: 'kuku', + execute_mistral.INPUT: {'kuku': 'get_attr('} + } + self._test_validation(content=template, expected_code=138) + + valid_action['execute_mistral'] = { + execute_mistral.WORKFLOW: 'kuku', + execute_mistral.INPUT: {'kuku': 'get_attr(host, name)'}, + } + self._test_validation(content=template, expected_code=0) + + def test_conditions(self): + self._test_validation(file='valid_conditions.yaml', expected_code=0) + template = self._get_yaml('valid_conditions.yaml') + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host AND NOT host host [contains] instance' + self._test_validation(content=template, expected_code=85) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host AND instance host [contains] instance' + self._test_validation(content=template, expected_code=85) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host AND NOT host [host contains] instance' + self._test_validation(content=template, expected_code=85) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host AND instance host [contains] instance' + self._test_validation(content=template, expected_code=85) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'NOT host [contains] instance' + self._test_validation(content=template, expected_code=134) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'NOT host [contains] instance AND NOT host [contains] instance' + self._test_validation(content=template, expected_code=134) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'NOT (host [ contains ] instance or host [ contains ] instance)' + self._test_validation(content=template, expected_code=134) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'NOT host_incorrect_key' + self._test_validation(content=template, expected_code=10200) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host_incorrect_key' + self._test_validation(content=template, expected_code=10200) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host [contains] instance_incorrect_key' + self._test_validation(content=template, expected_code=10200) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host_incorrect_key [contains] instance' + self._test_validation(content=template, expected_code=10200) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'NOT host' + self._test_validation(content=template, expected_code=86) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + '(host_incorrect_key)' + self._test_validation(content=template, expected_code=10200) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + '( host OR host [contains] instance ) AND' \ + ' (instance OR host [contains] instance' # missing parentheses + self._test_validation(content=template, expected_code=85) + + template[TF.SCENARIOS][0][TF.CONDITION] = 'host OR instance' + self._test_validation(content=template, expected_code=135) + + template[TF.SCENARIOS][0][TF.CONDITION] = \ + 'host OR NOT host [contains] instance' + self._test_validation(content=template, expected_code=135) + + def test_regex(self): + template = self._get_yaml('valid_actions.yaml') + template[TF.ENTITIES]['entity_regex'] = {'name.regex': 'bad.regex('} + self._test_validation(content=template, expected_code=47) + + def test_basic(self): + template = self._get_yaml('valid_conditions.yaml') + del template[TF.SCENARIOS] + self._test_validation(content=template, expected_code=80) + + template = self._get_yaml('valid_conditions.yaml') + del template[TF.ENTITIES] + self._test_validation(content=template, expected_code=10101) + + template = self._get_yaml('valid_conditions.yaml') + template['kuku'] = {} + self._test_validation(content=template, expected_code=4) diff --git a/vitrage/tests/unit/evaluator/test_condition.py b/vitrage/tests/unit/evaluator/test_condition.py index 6f79541b7..4664bfec6 100644 --- a/vitrage/tests/unit/evaluator/test_condition.py +++ b/vitrage/tests/unit/evaluator/test_condition.py @@ -14,11 +14,11 @@ from vitrage.evaluator.condition import SymbolResolver from vitrage.evaluator.template_data import EdgeDescription -from vitrage.evaluator.template_loading.template_loader import TemplateLoader from vitrage.evaluator.template_validation.content.v1.scenario_validator \ import get_condition_common_targets from vitrage.tests import base from vitrage.tests.mocks import utils +from vitrage.tests.unit.evaluator import get_template_data from vitrage.utils import file as file_utils @@ -93,7 +93,7 @@ class ConditionTest(base.BaseTest): template_name) template_definition = file_utils.load_yaml_file(template_path, True) - template_data = TemplateLoader().load(template_definition) + template_data = get_template_data(template_definition) definitions_index = template_data.entities.copy() definitions_index.update(template_data.relationships) diff --git a/vitrage/tests/unit/evaluator/test_template_loader.py b/vitrage/tests/unit/evaluator/test_template_loader.py index 9bfc3f71c..a93d63666 100644 --- a/vitrage/tests/unit/evaluator/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/test_template_loader.py @@ -27,11 +27,12 @@ from vitrage.evaluator.template_data import ActionSpecs from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_data import Scenario from vitrage.evaluator.template_fields import TemplateFields as TFields -from vitrage.evaluator.template_loading.template_loader import TemplateLoader +from vitrage.evaluator.template_loading.props_converter import PropsConverter from vitrage.graph import Edge from vitrage.graph import Vertex from vitrage.tests import base from vitrage.tests.mocks import utils +from vitrage.tests.unit.evaluator import get_template_data from vitrage.utils import file as file_utils @@ -56,8 +57,8 @@ class TemplateLoaderTest(base.BaseTest): def_templates_path) def_templates_dict = utils.get_def_templates_dict_from_list( def_demplates_list) - template_data = \ - TemplateLoader().load(template_definition, def_templates_dict) + template_data = get_template_data(template_definition, + def_templates_dict) entities = template_data.entities relationships = template_data.relationships scenarios = template_data.scenarios @@ -74,8 +75,8 @@ class TemplateLoaderTest(base.BaseTest): # Assertions for definition in definitions[TFields.ENTITIES]: for key, value in definition['entity'].items(): - new_key = TemplateLoader.PROPS_CONVERSION[key] if key in \ - TemplateLoader.PROPS_CONVERSION else key + new_key = PropsConverter.PROPS_CONVERSION[key] \ + if key in PropsConverter.PROPS_CONVERSION else key del definition['entity'][key] definition['entity'][new_key] = value self._validate_entities(entities, definitions[TFields.ENTITIES]) @@ -164,7 +165,7 @@ class TemplateLoaderTest(base.BaseTest): self.BASIC_TEMPLATE) template_definition = file_utils.load_yaml_file(template_path, True) - template_data = TemplateLoader().load(template_definition) + template_data = get_template_data(template_definition) entities = template_data.entities relationships = template_data.relationships scenarios = template_data.scenarios @@ -173,8 +174,8 @@ class TemplateLoaderTest(base.BaseTest): # Assertions for definition in definitions[TFields.ENTITIES]: for key, value in definition['entity'].items(): - new_key = TemplateLoader.PROPS_CONVERSION[key] if key in \ - TemplateLoader.PROPS_CONVERSION else key + new_key = PropsConverter.PROPS_CONVERSION[key] \ + if key in PropsConverter.PROPS_CONVERSION else key del definition['entity'][key] definition['entity'][new_key] = value self._validate_entities(entities, definitions[TFields.ENTITIES]) @@ -248,7 +249,7 @@ class TemplateLoaderTest(base.BaseTest): template_path = '%s/templates/version/%s' % (utils.get_resources_dir(), template_file) template_definition = file_utils.load_yaml_file(template_path, True) - template_data = TemplateLoader().load(template_definition) + template_data = get_template_data(template_definition) scenarios = template_data.scenarios self.assertIsNotNone(scenarios, 'Template should include a scenario') self.assertThat(scenarios, matchers.HasLength(1),