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
This commit is contained in:
Idan Hefetz 2019-01-24 13:01:40 +00:00
parent 10a7d7eb0d
commit a859882b47
28 changed files with 1607 additions and 121 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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))

View File

@ -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)

View File

@ -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())

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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',
}

View File

@ -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)):

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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),