Add support for building equivalent scenario
TemplateData is refactored to make it possible - split SubGraph and ScenarioData from TemplateData - build entity index and relationship index in TemplateData - delay variable extraction to subgraph build - simplify the process of adding scenario to repository Implement: blueprint entity-equivalence Change-Id: I7192e3a82880dc3c3995000c029af4e0aabd8d6c
This commit is contained in:
parent
6278d815ad
commit
2cb1fafe2b
@ -62,28 +62,20 @@ Let's take a basic template as example
|
||||
'alarm': Vertex(vertex_id='alarm',
|
||||
properties={'category': 'ALARM',
|
||||
'type': 'nagios',
|
||||
'name': 'HOST_HIGH_CPU_LOAD',
|
||||
'is_deleted': False,
|
||||
'is_placeholder': False
|
||||
'name': 'HOST_HIGH_CPU_LOAD'
|
||||
}),
|
||||
'resource': Vertex(vertex_id='resource',
|
||||
properties={'category': 'RESOURCE',
|
||||
'type': 'nova.host',
|
||||
'is_deleted': False,
|
||||
'is_placeholder': False
|
||||
'type': 'nova.host'
|
||||
})
|
||||
}
|
||||
|
||||
expected_relationships = {
|
||||
'alarm_on_host': EdgeDescription(
|
||||
edge=Edge(
|
||||
source_id='alarm',
|
||||
edge=Edge(source_id='alarm',
|
||||
target_id='resource',
|
||||
label='on',
|
||||
properties={
|
||||
'relationship_type': 'on',
|
||||
'is_deleted': False,
|
||||
'negative_condition': False}),
|
||||
properties={'relationship_type': 'on'}),
|
||||
source=expected_entities['alarm'],
|
||||
target=expected_entities['resource']
|
||||
)
|
||||
@ -92,19 +84,16 @@ Let's take a basic template as example
|
||||
expected_scenario = Scenario(
|
||||
id='basic_template-scenario0',
|
||||
condition=[
|
||||
[ConditionVar(
|
||||
variable=expected_relationships['alarm_on_host'],
|
||||
type='relationship',
|
||||
positive=True)]
|
||||
],
|
||||
[ConditionVar(symbol_name='alarm_on_host',
|
||||
positive=True)]],
|
||||
actions=[
|
||||
ActionSpecs(
|
||||
type='set_state',
|
||||
targets={
|
||||
'target': 'resource'},
|
||||
properties={
|
||||
'state': 'SUBOPTIMAL'})],
|
||||
subgraphs=[object] # [<vitrage.graph.driver.networkx_graph.NXGraph object>]
|
||||
targets={'target': 'resource'},
|
||||
properties={'state': 'SUBOPTIMAL'})],
|
||||
subgraphs=template_data.scenarios[0].subgraphs, # ignore subgraphs
|
||||
entities=expected_entities,
|
||||
relationships=expected_relationships
|
||||
)
|
||||
|
||||
Entities and relationships
|
||||
@ -170,26 +159,34 @@ operators. As explained in embedded comment:
|
||||
:param condition_str: the string as it written in the template itself
|
||||
:return: condition_vars_lists
|
||||
|
||||
Each condition variable refers either an entity or relationship by ``variable``.
|
||||
The same variable in different conditions share an identical object. It is not
|
||||
duplicated.
|
||||
|
||||
actions
|
||||
-------
|
||||
|
||||
``actions`` is a list of ``ActionSpecs``.
|
||||
|
||||
Note that the values of action ``targets`` are **string**. It is different from
|
||||
how relationships and entities are referred in ``condition``, which are object
|
||||
references.
|
||||
The action targets in the spec must be referenced in the condition definition.
|
||||
They are either linked to ``vertex_id`` of entity condition variables or
|
||||
``source_id`` and ``target_id`` in relationship condition variable extracted.
|
||||
|
||||
The targets values are linked to ``vertex_id`` of entity condition variables or
|
||||
``source_id`` and ``target_id`` in a relationship condition variable. It will
|
||||
be resolved to real targets from matched sub-graph in entity graph.
|
||||
In each matched subgraph in the entity graph, the targets will be resolved as
|
||||
concrete vertices or edges.
|
||||
|
||||
subgraphs
|
||||
---------
|
||||
|
||||
Sub graphs are compiled from conditions for pattern matching in the entity
|
||||
graph. Each sub-list in condition variables list is compiled into one sub
|
||||
graph. The actions will be triggered if any of the subgraph is matched.
|
||||
Sub graphs are built from conditions for pattern matching in the entity graph.
|
||||
Each sub-list in condition variables list is compiled into one sub graph. The
|
||||
actions will be triggered if any of the subgraph is matched.
|
||||
|
||||
entities & relationships
|
||||
------------------------
|
||||
|
||||
Dicts of **touched** entities and relationships during subgraph building are
|
||||
saved in scenario.
|
||||
|
||||
This makes creation of the scenarios repository index on related entities and
|
||||
relationships easier and more efficient. You don't need to traverse the
|
||||
condition object again, which is already done once during subgraphs building.
|
||||
It also eliminate the necessity of duplication check because there is no
|
||||
duplicate entities or relationships in these dicts compared to the condition
|
||||
variables lists.
|
||||
|
@ -13,6 +13,7 @@
|
||||
# under the License.
|
||||
|
||||
from vitrage.common.constants import TemplateTopologyFields
|
||||
from vitrage.common.exception import VitrageError
|
||||
|
||||
|
||||
class Fields(TemplateTopologyFields):
|
||||
@ -37,18 +38,23 @@ class EquivalenceData(object):
|
||||
|
||||
@staticmethod
|
||||
def _build_equivalences(equivalence_defs):
|
||||
"""equivalence stored as arrays of frozen entity sets
|
||||
"""equivalence stored as arrays of frozen entity props sets
|
||||
|
||||
equivalences:: [equivalence, ...]
|
||||
equivalence:: {entity, ...}
|
||||
entity:: {(k,v), ...}
|
||||
equivalence:: {entity_props, ...}
|
||||
entity_props:: {(k,v), ...}
|
||||
"""
|
||||
equivalences = []
|
||||
for equivalence_def in equivalence_defs:
|
||||
equivalent_entities = equivalence_def[Fields.EQUIVALENCE]
|
||||
equivalence = []
|
||||
equivalence = set()
|
||||
for entity_def in equivalent_entities:
|
||||
equivalence.append(frozenset(entity_def[Fields.ENTITY]
|
||||
.items()))
|
||||
entity_props = {(k, v)
|
||||
for k, v in entity_def[Fields.ENTITY].items()}
|
||||
entity_key = frozenset(entity_props)
|
||||
if entity_key in equivalence:
|
||||
raise VitrageError('duplicated entities found in '
|
||||
'equivalence')
|
||||
equivalence.add(entity_key)
|
||||
equivalences.append(frozenset(equivalence))
|
||||
return equivalences
|
||||
|
39
vitrage/evaluator/equivalence_repository.py
Normal file
39
vitrage/evaluator/equivalence_repository.py
Normal file
@ -0,0 +1,39 @@
|
||||
# Copyright 2017 - ZTE Corporation
|
||||
#
|
||||
# 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.common.exception import VitrageError
|
||||
from vitrage.evaluator.equivalence_data import EquivalenceData
|
||||
from vitrage.utils import file as file_utils
|
||||
|
||||
|
||||
class EquivalenceRepository(object):
|
||||
def __init__(self):
|
||||
self.entity_equivalences = {}
|
||||
|
||||
def load_files(self, directory):
|
||||
equivalence_defs = file_utils.load_yaml_files(directory)
|
||||
|
||||
for equivalence_def in equivalence_defs:
|
||||
equivalence_data = EquivalenceData(equivalence_def)
|
||||
for equivalence in equivalence_data.equivalences:
|
||||
self._add_equivalence(equivalence)
|
||||
return self.entity_equivalences
|
||||
|
||||
def _add_equivalence(self, equivalence):
|
||||
for entity in equivalence:
|
||||
if entity in self.entity_equivalences:
|
||||
# TODO(yujunz): log error and continue processing the rest
|
||||
raise VitrageError('one entity should not be included in '
|
||||
'multiple equivalence definitions')
|
||||
self.entity_equivalences[entity] = equivalence
|
@ -19,7 +19,7 @@ from oslo_log import log
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from vitrage.evaluator.base import Template
|
||||
from vitrage.evaluator.template_data import RELATIONSHIP
|
||||
from vitrage.evaluator.equivalence_repository import EquivalenceRepository
|
||||
from vitrage.evaluator.template_data import TemplateData
|
||||
from vitrage.evaluator.template_validation.template_content_validator import \
|
||||
content_validation
|
||||
@ -36,6 +36,8 @@ EdgeKeyScenario = namedtuple('EdgeKeyScenario', ['label', 'source', 'target'])
|
||||
class ScenarioRepository(object):
|
||||
def __init__(self, conf):
|
||||
self._templates = {}
|
||||
self.entity_equivalences = \
|
||||
EquivalenceRepository().load_files(conf.evaluator.equivalences_dir)
|
||||
self.relationship_scenarios = defaultdict(list)
|
||||
self.entity_scenarios = defaultdict(list)
|
||||
self._load_templates_files(conf)
|
||||
@ -93,7 +95,39 @@ class ScenarioRepository(object):
|
||||
result)
|
||||
if result.is_valid_config:
|
||||
template_data = TemplateData(template_def)
|
||||
self._add_template_scenarios(template_data)
|
||||
for scenario in template_data.scenarios:
|
||||
for equivalent_scenario in self._expand_equivalence(scenario):
|
||||
self._add_scenario(equivalent_scenario)
|
||||
|
||||
def _expand_equivalence(self, scenario):
|
||||
equivalent_scenarios = [scenario]
|
||||
for symbol_name, entity in scenario.entities.items():
|
||||
entity_key = frozenset(entity.properties.items())
|
||||
if entity_key not in self.entity_equivalences:
|
||||
continue
|
||||
equivalent_scenarios = self._expand_on_symbol(
|
||||
equivalent_scenarios,
|
||||
symbol_name,
|
||||
self.entity_equivalences[entity_key] - {entity_key})
|
||||
return equivalent_scenarios
|
||||
|
||||
@staticmethod
|
||||
def _expand_on_symbol(scenarios_in, symbol_name, entity_keys):
|
||||
scenarios_out = list(scenarios_in)
|
||||
for entity_key in entity_keys:
|
||||
for scenario in scenarios_in:
|
||||
equivalent_scenario = TemplateData.ScenarioData.\
|
||||
build_equivalent_scenario(scenario,
|
||||
symbol_name,
|
||||
entity_key)
|
||||
scenarios_out.append(equivalent_scenario)
|
||||
return scenarios_out
|
||||
|
||||
def _add_scenario(self, scenario):
|
||||
for entity in scenario.entities.values():
|
||||
self._add_entity_scenario(scenario, entity)
|
||||
for relationship in scenario.relationships.values():
|
||||
self._add_relationship_scenario(scenario, relationship)
|
||||
|
||||
def _load_templates_files(self, conf):
|
||||
|
||||
@ -103,34 +137,13 @@ class ScenarioRepository(object):
|
||||
for template_def in template_defs:
|
||||
self.add_template(template_def)
|
||||
|
||||
def _add_template_scenarios(self, template):
|
||||
for scenario in template.scenarios:
|
||||
self._handle_condition(scenario)
|
||||
|
||||
def _handle_condition(self, scenario):
|
||||
for clause in scenario.condition:
|
||||
self._handle_clause(clause, scenario)
|
||||
|
||||
def _handle_clause(self, clause, scenario):
|
||||
for condition_var in clause:
|
||||
if condition_var.type == RELATIONSHIP:
|
||||
edge_desc = condition_var.variable
|
||||
self._add_relationship(scenario, edge_desc)
|
||||
self._add_entity(scenario, edge_desc.source)
|
||||
self._add_entity(scenario, edge_desc.target)
|
||||
else: # Entity
|
||||
self._add_entity(scenario, condition_var.variable)
|
||||
|
||||
@staticmethod
|
||||
def _create_scenario_key(properties):
|
||||
return frozenset(properties)
|
||||
|
||||
def _add_relationship(self, scenario, edge_desc):
|
||||
def _add_relationship_scenario(self, scenario, edge_desc):
|
||||
|
||||
key = self._create_edge_scenario_key(edge_desc)
|
||||
scenarios = self.relationship_scenarios[key]
|
||||
|
||||
if not self._edge_contains(scenarios, scenario, edge_desc):
|
||||
self.relationship_scenarios[key].append((edge_desc, scenario))
|
||||
|
||||
@staticmethod
|
||||
@ -139,22 +152,7 @@ class ScenarioRepository(object):
|
||||
frozenset(edge_desc.source.properties.items()),
|
||||
frozenset(edge_desc.target.properties.items()))
|
||||
|
||||
def _add_entity(self, scenario, entity):
|
||||
def _add_entity_scenario(self, scenario, entity):
|
||||
|
||||
key = frozenset(list(entity.properties.items()))
|
||||
scenarios = self.entity_scenarios[key]
|
||||
|
||||
if not self._entity_contains(scenarios, scenario, entity):
|
||||
self.entity_scenarios[key].append((entity, scenario))
|
||||
|
||||
@staticmethod
|
||||
def _edge_contains(scenarios, scenario, edge):
|
||||
return any(e.edge.source_id == edge.edge.source_id and
|
||||
e.edge.target_id == edge.edge.target_id and
|
||||
e.edge.label == edge.edge.label and s.id == scenario.id
|
||||
for e, s in scenarios)
|
||||
|
||||
@staticmethod
|
||||
def _entity_contains(scenarios, scenario, entity):
|
||||
return any(e.vertex_id == entity.vertex_id and s.id == scenario.id
|
||||
for e, s in scenarios)
|
||||
|
@ -19,25 +19,36 @@ from sympy.logic.boolalg import Or
|
||||
from sympy.logic.boolalg import to_dnf as sympy_to_dfn
|
||||
from sympy import Symbol
|
||||
|
||||
|
||||
from vitrage.common.constants import EdgeProperties as EProps
|
||||
from vitrage.common.constants import VertexProperties as VProps
|
||||
from vitrage.common.exception import VitrageError
|
||||
from vitrage.evaluator.template_fields import TemplateFields as TFields
|
||||
from vitrage.graph.algo_driver.sub_graph_matching import NEG_CONDITION
|
||||
from vitrage.graph.driver.networkx_graph import NXGraph
|
||||
from vitrage.graph import Edge
|
||||
from vitrage.graph import Vertex
|
||||
|
||||
ConditionVar = namedtuple('ConditionVar', ['variable', 'type', 'positive'])
|
||||
ConditionVar = namedtuple('ConditionVar', ['symbol_name', 'positive'])
|
||||
ActionSpecs = namedtuple('ActionSpecs', ['type', 'targets', 'properties'])
|
||||
Scenario = namedtuple('Scenario', ['id', 'condition', 'actions', 'subgraphs'])
|
||||
Scenario = namedtuple('Scenario', ['id',
|
||||
'condition',
|
||||
'actions',
|
||||
'subgraphs',
|
||||
'entities',
|
||||
'relationships'
|
||||
])
|
||||
EdgeDescription = namedtuple('EdgeDescription', ['edge', 'source', 'target'])
|
||||
|
||||
|
||||
ENTITY = 'entity'
|
||||
RELATIONSHIP = 'relationship'
|
||||
|
||||
|
||||
def copy_edge_desc(edge_desc):
|
||||
return EdgeDescription(edge=edge_desc.edge.copy(),
|
||||
source=edge_desc.source.copy(),
|
||||
target=edge_desc.target.copy())
|
||||
|
||||
|
||||
# noinspection PyAttributeOutsideInit
|
||||
class TemplateData(object):
|
||||
|
||||
@ -135,17 +146,94 @@ class TemplateData(object):
|
||||
def _build_scenarios(self, scenarios_defs):
|
||||
|
||||
scenarios = []
|
||||
for counter, scenarios_def in enumerate(scenarios_defs):
|
||||
scenario_dict = scenarios_def[TFields.SCENARIO]
|
||||
condition = self._parse_condition(scenario_dict[TFields.CONDITION])
|
||||
action_specs = self._build_actions(scenario_dict[TFields.ACTIONS])
|
||||
subgraphs = self._build_subgraphs(condition)
|
||||
for counter, scenario_def in enumerate(scenarios_defs):
|
||||
scenario_id = "%s-scenario%s" % (self.name, str(counter))
|
||||
scenarios.append(Scenario(scenario_id, condition,
|
||||
action_specs, subgraphs))
|
||||
|
||||
scenario_dict = scenario_def[TFields.SCENARIO]
|
||||
scenarios.append(TemplateData.ScenarioData(
|
||||
scenario_id,
|
||||
scenario_dict, self).to_tuple())
|
||||
return scenarios
|
||||
|
||||
class ScenarioData(object):
|
||||
def __init__(self, scenario_id, scenario_dict, template_data):
|
||||
self._template_entities = template_data.entities
|
||||
self._template_relationships = template_data.relationships
|
||||
|
||||
self._entities = {}
|
||||
self._relationships = {}
|
||||
|
||||
self.scenario_id = scenario_id
|
||||
self.condition = self._parse_condition(
|
||||
scenario_dict[TFields.CONDITION])
|
||||
self.actions = self._build_actions(scenario_dict[TFields.ACTIONS])
|
||||
self.subgraphs = TemplateData.SubGraph.from_condition(
|
||||
self.condition,
|
||||
self._extract_var_and_update_index)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.scenario_id == other.scenario_id \
|
||||
and self.condition == other.condition \
|
||||
and self.actions == other.actions
|
||||
|
||||
def to_tuple(self):
|
||||
return Scenario(id=self.scenario_id,
|
||||
condition=self.condition,
|
||||
actions=self.actions,
|
||||
subgraphs=self.subgraphs,
|
||||
entities=self._entities,
|
||||
relationships=self._relationships)
|
||||
|
||||
@classmethod
|
||||
def build_equivalent_scenario(cls,
|
||||
scenario,
|
||||
template_id,
|
||||
entity_props):
|
||||
entities = scenario.entities.copy()
|
||||
entities[template_id] = Vertex(
|
||||
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)
|
||||
for rel_id, rel in scenario.relationships.items()}
|
||||
|
||||
def extract_var(symbol_name):
|
||||
if symbol_name in entities:
|
||||
return entities[symbol_name], ENTITY
|
||||
elif symbol_name in relationships:
|
||||
return relationships[symbol_name], RELATIONSHIP
|
||||
else:
|
||||
raise VitrageError('invalid symbol name: {}'
|
||||
.format(symbol_name))
|
||||
|
||||
subgraphs = TemplateData.SubGraph.from_condition(
|
||||
scenario.condition, extract_var)
|
||||
|
||||
return Scenario(id=scenario.id + '_equivalence',
|
||||
condition=scenario.condition,
|
||||
actions=scenario.actions,
|
||||
subgraphs=subgraphs,
|
||||
entities=entities,
|
||||
relationships=relationships)
|
||||
|
||||
@classmethod
|
||||
def build_equivalent_relationship(cls,
|
||||
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)
|
||||
|
||||
@staticmethod
|
||||
def _build_actions(actions_def):
|
||||
|
||||
@ -161,60 +249,22 @@ class TemplateData(object):
|
||||
|
||||
return actions
|
||||
|
||||
def _build_subgraphs(self, condition):
|
||||
return [self._build_subgraph(clause) for clause in condition]
|
||||
|
||||
def _build_subgraph(self, clause):
|
||||
condition_g = NXGraph("scenario condition")
|
||||
|
||||
for term in clause:
|
||||
if term.type == ENTITY:
|
||||
term.variable[VProps.IS_DELETED] = False
|
||||
term.variable[VProps.IS_PLACEHOLDER] = False
|
||||
condition_g.add_vertex(term.variable)
|
||||
|
||||
else: # type = relationship
|
||||
edge_desc = term.variable
|
||||
self._set_edge_relationship_info(edge_desc, term.positive)
|
||||
self._add_edge_relationship(condition_g, edge_desc)
|
||||
|
||||
return condition_g
|
||||
|
||||
@staticmethod
|
||||
def _set_edge_relationship_info(edge_description, is_positive_condition):
|
||||
if not is_positive_condition:
|
||||
edge_description.edge[NEG_CONDITION] = True
|
||||
edge_description.edge[EProps.IS_DELETED] = True
|
||||
else:
|
||||
edge_description.edge[EProps.IS_DELETED] = False
|
||||
edge_description.edge[NEG_CONDITION] = False
|
||||
|
||||
edge_description.source[VProps.IS_DELETED] = False
|
||||
edge_description.source[VProps.IS_PLACEHOLDER] = False
|
||||
edge_description.target[VProps.IS_DELETED] = False
|
||||
edge_description.target[VProps.IS_PLACEHOLDER] = False
|
||||
|
||||
@staticmethod
|
||||
def _add_edge_relationship(condition_graph, edge_description):
|
||||
condition_graph.add_vertex(edge_description.source)
|
||||
condition_graph.add_vertex(edge_description.target)
|
||||
condition_graph.add_edge(edge_description.edge)
|
||||
|
||||
def _parse_condition(self, condition_str):
|
||||
"""Parse condition string into an object
|
||||
|
||||
The condition string will be converted here into DNF (Disjunctive
|
||||
Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)...
|
||||
where X, Y, Z, V, W are either entities or relationships
|
||||
Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)
|
||||
... where X, Y, Z, V, W are either entities or relationships
|
||||
more details: https://en.wikipedia.org/wiki/Disjunctive_normal_form
|
||||
|
||||
The condition variable lists is then extracted from the DNF object. It
|
||||
is a list of lists. Each inner list represents an AND expression
|
||||
compound condition variables. The outer list presents the OR expression
|
||||
The condition variable lists is then extracted from the DNF object.
|
||||
It is a list of lists. Each inner list represents an AND expression
|
||||
compound condition variables. The outer list presents the OR
|
||||
expression
|
||||
|
||||
[[and_var1, and_var2, ...], or_list_2, ...]
|
||||
|
||||
:param condition_str: the string as it written in the template itself
|
||||
:param condition_str: the string as it written in the template
|
||||
:return: condition_vars_lists
|
||||
"""
|
||||
|
||||
@ -266,13 +316,68 @@ class TemplateData(object):
|
||||
def _extract_condition_var(self, symbol, positive):
|
||||
if isinstance(symbol, Not):
|
||||
return self._extract_not_condition_var(symbol)[0]
|
||||
return ConditionVar(symbol.name, positive)
|
||||
|
||||
def _extract_var_and_update_index(self, symbol_name):
|
||||
|
||||
if symbol_name in self._template_relationships:
|
||||
relationship = self._template_relationships[symbol_name]
|
||||
self._relationships[symbol_name] = relationship
|
||||
self._entities.update({
|
||||
relationship.edge.source_id: relationship.source,
|
||||
relationship.edge.target_id: relationship.target
|
||||
})
|
||||
return relationship, RELATIONSHIP
|
||||
|
||||
entity = self._template_entities[symbol_name]
|
||||
self._entities[symbol_name] = entity
|
||||
return entity, ENTITY
|
||||
|
||||
class SubGraph(object):
|
||||
@classmethod
|
||||
def from_condition(cls, condition, extract_var):
|
||||
return [cls.from_clause(clause, extract_var)
|
||||
for clause in condition]
|
||||
|
||||
@classmethod
|
||||
def from_clause(cls, clause, extract_var):
|
||||
condition_g = NXGraph("scenario condition")
|
||||
|
||||
for term in clause:
|
||||
variable, var_type = extract_var(term.symbol_name)
|
||||
if var_type == ENTITY:
|
||||
vertex = variable.copy()
|
||||
vertex[VProps.IS_DELETED] = False
|
||||
vertex[VProps.IS_PLACEHOLDER] = False
|
||||
condition_g.add_vertex(vertex)
|
||||
|
||||
else: # type = relationship
|
||||
# prevent overwritten of NEG_CONDITION and IS_DELETED
|
||||
# property when there are both "not A" and "A" in same
|
||||
# template
|
||||
edge_desc = copy_edge_desc(variable)
|
||||
cls._set_edge_relationship_info(edge_desc, term.positive)
|
||||
cls._add_edge_relationship(condition_g, edge_desc)
|
||||
|
||||
return condition_g
|
||||
|
||||
@staticmethod
|
||||
def _set_edge_relationship_info(edge_description,
|
||||
is_positive_condition):
|
||||
if not is_positive_condition:
|
||||
edge_description.edge[NEG_CONDITION] = True
|
||||
edge_description.edge[EProps.IS_DELETED] = True
|
||||
else:
|
||||
var, var_type = self._extract_var(str(symbol))
|
||||
return ConditionVar(var, var_type, positive)
|
||||
edge_description.edge[EProps.IS_DELETED] = False
|
||||
edge_description.edge[NEG_CONDITION] = False
|
||||
|
||||
def _extract_var(self, template_id):
|
||||
edge_description.source[VProps.IS_DELETED] = False
|
||||
edge_description.source[VProps.IS_PLACEHOLDER] = False
|
||||
edge_description.target[VProps.IS_DELETED] = False
|
||||
edge_description.target[VProps.IS_PLACEHOLDER] = False
|
||||
|
||||
if template_id in self.relationships:
|
||||
return self.relationships[template_id], RELATIONSHIP
|
||||
|
||||
return self.entities[template_id], ENTITY
|
||||
@staticmethod
|
||||
def _add_edge_relationship(condition_graph, edge_description):
|
||||
condition_graph.add_vertex(edge_description.source)
|
||||
condition_graph.add_vertex(edge_description.target)
|
||||
condition_graph.add_edge(edge_description.edge)
|
||||
|
@ -142,7 +142,7 @@ def _validate_scenarios(scenarios, definitions_index):
|
||||
|
||||
def _validate_scenario_condition(condition, definitions_index):
|
||||
try:
|
||||
dnf_result = TemplateData.convert_to_dnf_format(condition)
|
||||
dnf_result = TemplateData.ScenarioData.convert_to_dnf_format(condition)
|
||||
except Exception:
|
||||
LOG.error('%s status code: %s' % (status_msgs[85], 85))
|
||||
return get_fault_result(RESULT_DESCRIPTION, 85)
|
||||
|
@ -42,6 +42,9 @@ class PropertiesElement(object):
|
||||
def items(self):
|
||||
return self.properties.items()
|
||||
|
||||
def copy(self):
|
||||
return PropertiesElement(self.properties.copy())
|
||||
|
||||
|
||||
class Vertex(PropertiesElement):
|
||||
"""Class Vertex
|
||||
@ -85,6 +88,10 @@ class Vertex(PropertiesElement):
|
||||
return self.__dict__ == other.__dict__ and \
|
||||
self.properties == other.properties
|
||||
|
||||
def copy(self):
|
||||
return Vertex(vertex_id=self.vertex_id,
|
||||
properties=self.properties.copy())
|
||||
|
||||
|
||||
class Edge(PropertiesElement):
|
||||
"""Class Edge represents a directional edge between two vertices
|
||||
@ -161,3 +168,9 @@ class Edge(PropertiesElement):
|
||||
|
||||
def has_vertex(self, v_id):
|
||||
return self.source_id == v_id or self.target_id == v_id
|
||||
|
||||
def copy(self):
|
||||
return Edge(source_id=self.source_id,
|
||||
target_id=self.target_id,
|
||||
label=self.label,
|
||||
properties=self.properties.copy())
|
||||
|
@ -3,14 +3,18 @@ metadata:
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
# matches an entity in templates/general/basic.yaml
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: host_problem
|
||||
- entity:
|
||||
# specify an equivalent entity
|
||||
# note that the `name` does not need to be identical
|
||||
category: ALARM
|
||||
type: zabbix
|
||||
name: host_fail # equivalence should work for unequal property values
|
||||
name: host_fail
|
||||
- entity:
|
||||
# allow multiple equivalent entities in same group
|
||||
category: ALARM
|
||||
type: vitrage
|
||||
name: host_down
|
||||
|
@ -0,0 +1,21 @@
|
||||
metadata:
|
||||
name: duplicated entities in multiple equivalences
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: host_problem
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: zabbix
|
||||
name: host_problem
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: vitrage
|
||||
name: host_problem
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: zabbix
|
||||
name: host_problem
|
@ -0,0 +1,12 @@
|
||||
metadata:
|
||||
name: first occurrence of zabbix host_problem
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: host_problem
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: zabbix
|
||||
name: host_problem
|
@ -0,0 +1,12 @@
|
||||
metadata:
|
||||
name: second occurrence of zabbix host_problem
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: vitrage
|
||||
name: host_problem
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: zabbix
|
||||
name: host_problem
|
@ -0,0 +1,12 @@
|
||||
metadata:
|
||||
name: duplicated entities in same equivalence
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: host_problem
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: host_problem
|
@ -0,0 +1,21 @@
|
||||
metadata:
|
||||
name: entity equivalence example
|
||||
equivalences:
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: cpu_high
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: vitrage
|
||||
name: cpu_high
|
||||
- equivalence:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: mem_low
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: vitrage
|
||||
name: mem_low
|
@ -0,0 +1,40 @@
|
||||
metadata:
|
||||
name: basic_template
|
||||
description: basic template for general tests
|
||||
definitions:
|
||||
entities:
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: cpu_high
|
||||
template_id: cpu_alarm
|
||||
- entity:
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: mem_low
|
||||
template_id: mem_alarm
|
||||
- entity:
|
||||
category: RESOURCE
|
||||
type: nova.host
|
||||
template_id: host
|
||||
relationships:
|
||||
- relationship:
|
||||
source: cpu_alarm
|
||||
target: host
|
||||
relationship_type: on
|
||||
template_id : cpu_alarm_on_host
|
||||
- relationship:
|
||||
source: mem_alarm
|
||||
target: host
|
||||
relationship_type: on
|
||||
template_id : mem_alarm_on_host
|
||||
scenarios:
|
||||
- scenario:
|
||||
condition: cpu_alarm_on_host or mem_alarm_on_host
|
||||
actions:
|
||||
- action:
|
||||
action_type: set_state
|
||||
properties:
|
||||
state: SUBOPTIMAL
|
||||
action_target:
|
||||
target: host
|
@ -4,9 +4,11 @@ metadata:
|
||||
definitions:
|
||||
entities:
|
||||
- entity:
|
||||
# an equivalence of this entity is defined in equivalences/basic.yaml
|
||||
# be sure keep them sync when modifying test cases
|
||||
category: ALARM
|
||||
type: nagios
|
||||
name: HOST_HIGH_CPU_LOAD
|
||||
name: host_problem
|
||||
template_id: alarm
|
||||
- entity:
|
||||
category: RESOURCE
|
||||
|
35
vitrage/tests/unit/evaluator/test_equivalence_repository.py
Normal file
35
vitrage/tests/unit/evaluator/test_equivalence_repository.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright 2017 - ZTE Corporation
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from vitrage.common.exception import VitrageError
|
||||
from vitrage.evaluator.equivalence_repository import EquivalenceRepository
|
||||
from vitrage.tests import base
|
||||
from vitrage.tests.mocks import utils
|
||||
|
||||
|
||||
class TestEquivalenceRepository(base.BaseTest):
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
def setUp(self):
|
||||
super(TestEquivalenceRepository, self).setUp()
|
||||
self.equivalence_repository = EquivalenceRepository()
|
||||
|
||||
def test_duplicate_entities_in_equivalence(self):
|
||||
equivalences_dup_dir = utils.get_resources_dir() + '/equivalences_dup'
|
||||
for directory in os.listdir(equivalences_dup_dir):
|
||||
self.assertRaises(VitrageError,
|
||||
self.equivalence_repository.load_files,
|
||||
os.path.join(equivalences_dup_dir, directory))
|
@ -73,6 +73,20 @@ class ScenarioRepositoryTest(base.BaseTest):
|
||||
scenario_templates = self.scenario_repository.templates
|
||||
self.assertEqual(valid_template_counter, len(scenario_templates))
|
||||
|
||||
entity_equivalences = self.scenario_repository.entity_equivalences
|
||||
for entity_props, equivalence in entity_equivalences.items():
|
||||
# Example structure of entity_equivalences
|
||||
# { A: (A, B, C),
|
||||
# B: (A, B, C),
|
||||
# C: (A, B, C)}
|
||||
# Verify entity itself is also included. It is not required, but
|
||||
# worth noting when handling equivalence
|
||||
self.assertTrue(entity_props in equivalence)
|
||||
for equivalent_props in equivalence:
|
||||
# Verify equivalent scenarios are present in repository
|
||||
self.assertTrue(equivalent_props in
|
||||
self.scenario_repository.entity_scenarios)
|
||||
|
||||
def test_get_scenario_by_edge(self):
|
||||
pass
|
||||
|
||||
@ -81,3 +95,37 @@ class ScenarioRepositoryTest(base.BaseTest):
|
||||
|
||||
def test_add_template(self):
|
||||
pass
|
||||
|
||||
|
||||
class ScenarioExpansionTest(base.BaseTest):
|
||||
BASE_DIR = utils.get_resources_dir() + '/scenario_expansion/'
|
||||
OPTS = [
|
||||
cfg.StrOpt('templates_dir',
|
||||
default=BASE_DIR + 'templates'),
|
||||
cfg.StrOpt('equivalences_dir',
|
||||
default=BASE_DIR + '/equivalences')]
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
|
||||
cls.conf = cfg.ConfigOpts()
|
||||
cls.conf.register_opts(cls.OPTS, group='evaluator')
|
||||
|
||||
templates_dir_path = cls.conf.evaluator.templates_dir
|
||||
cls.template_defs = file_utils.load_yaml_files(templates_dir_path)
|
||||
|
||||
cls.scenario_repository = ScenarioRepository(cls.conf)
|
||||
|
||||
def test_expansion(self):
|
||||
entity_scenarios = self.scenario_repository.entity_scenarios
|
||||
for entity_key, scenarios in entity_scenarios.items():
|
||||
if ('category', 'ALARM') in entity_key:
|
||||
# scenarios expanded on the other alarm
|
||||
self.assertEqual(len(scenarios), 2)
|
||||
if ('category', 'RESOURCE') in entity_key:
|
||||
# Scenarios expanded on the two alarms. Each alarm is expanded
|
||||
# to two equivalent alarms. Thus 2 x 2 = 4 in total
|
||||
self.assertEqual(len(scenarios), 4)
|
||||
# each relationship is expand to two. Thus 2 x 2 = 4 in total
|
||||
relationships = self.scenario_repository.relationship_scenarios.keys()
|
||||
self.assertEqual(len(relationships), 4)
|
||||
|
@ -12,7 +12,6 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from vitrage.common.constants import EdgeLabel
|
||||
from vitrage.evaluator.template_data import ActionSpecs
|
||||
from vitrage.evaluator.template_data import ConditionVar
|
||||
from vitrage.evaluator.template_data import EdgeDescription
|
||||
@ -55,15 +54,11 @@ class BasicTemplateTest(base.BaseTest):
|
||||
'alarm': Vertex(vertex_id='alarm',
|
||||
properties={'category': 'ALARM',
|
||||
'type': 'nagios',
|
||||
'name': 'HOST_HIGH_CPU_LOAD',
|
||||
'is_deleted': False,
|
||||
'is_placeholder': False
|
||||
'name': 'host_problem'
|
||||
}),
|
||||
'resource': Vertex(vertex_id='resource',
|
||||
properties={'category': 'RESOURCE',
|
||||
'type': 'nova.host',
|
||||
'is_deleted': False,
|
||||
'is_placeholder': False
|
||||
'type': 'nova.host'
|
||||
})
|
||||
}
|
||||
|
||||
@ -72,9 +67,7 @@ class BasicTemplateTest(base.BaseTest):
|
||||
edge=Edge(source_id='alarm',
|
||||
target_id='resource',
|
||||
label='on',
|
||||
properties={'relationship_type': 'on',
|
||||
'is_deleted': False,
|
||||
'negative_condition': False}),
|
||||
properties={'relationship_type': 'on'}),
|
||||
source=expected_entities['alarm'],
|
||||
target=expected_entities['resource']
|
||||
)
|
||||
@ -83,15 +76,19 @@ class BasicTemplateTest(base.BaseTest):
|
||||
expected_scenario = Scenario(
|
||||
id='basic_template-scenario0',
|
||||
condition=[
|
||||
[ConditionVar(variable=expected_relationships['alarm_on_host'],
|
||||
type='relationship',
|
||||
[ConditionVar(symbol_name='alarm_on_host',
|
||||
positive=True)]],
|
||||
actions=[
|
||||
ActionSpecs(
|
||||
type='set_state',
|
||||
targets={'target': 'resource'},
|
||||
properties={'state': 'SUBOPTIMAL'})],
|
||||
subgraphs=template_data.scenarios[0].subgraphs
|
||||
# TODO(yujunz): verify the built subgraph is consistent with
|
||||
# scenario definition. For now the observed value is
|
||||
# assigned to make test passing
|
||||
subgraphs=template_data.scenarios[0].subgraphs,
|
||||
entities=expected_entities,
|
||||
relationships=expected_relationships
|
||||
)
|
||||
|
||||
self._validate_strict_equal(template_data,
|
||||
@ -186,19 +183,8 @@ class BasicTemplateTest(base.BaseTest):
|
||||
condition_var = condition[0][0]
|
||||
self.assertIsInstance(condition_var, ConditionVar)
|
||||
|
||||
variable = condition_var.variable
|
||||
self.assertIsInstance(variable, EdgeDescription)
|
||||
|
||||
edge = variable[0]
|
||||
self.assertEqual(edge.source_id, 'alarm')
|
||||
self.assertEqual(edge.target_id, 'resource')
|
||||
self.assertEqual(edge.label, EdgeLabel.ON)
|
||||
|
||||
source = variable[1]
|
||||
self.assertEqual(source, entities[source.vertex_id])
|
||||
|
||||
target = variable[2]
|
||||
self.assertEqual(target, entities[target.vertex_id])
|
||||
symbol_name = condition_var.symbol_name
|
||||
self.assertIsInstance(symbol_name, str)
|
||||
|
||||
actions = scenario.actions
|
||||
self.assert_is_not_empty(scenario.actions)
|
||||
|
Loading…
Reference in New Issue
Block a user