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:
Yujun Zhang 2017-05-04 19:53:13 +08:00
parent 6278d815ad
commit 2cb1fafe2b
18 changed files with 576 additions and 225 deletions

View File

@ -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',
target_id='resource',
label='on',
properties={
'relationship_type': 'on',
'is_deleted': False,
'negative_condition': False}),
edge=Edge(source_id='alarm',
target_id='resource',
label='on',
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.

View File

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

View 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

View File

@ -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,35 +137,14 @@ 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))
self.relationship_scenarios[key].append((edge_desc, scenario))
@staticmethod
def _create_edge_scenario_key(edge_desc):
@ -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)
self.entity_scenarios[key].append((entity, scenario))

View File

@ -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,144 +146,238 @@ 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
@staticmethod
def _build_actions(actions_def):
class ScenarioData(object):
def __init__(self, scenario_id, scenario_dict, template_data):
self._template_entities = template_data.entities
self._template_relationships = template_data.relationships
actions = []
for action_def in actions_def:
self._entities = {}
self._relationships = {}
action_dict = action_def[TFields.ACTION]
action_type = action_dict[TFields.ACTION_TYPE]
targets = action_dict[TFields.ACTION_TARGET]
properties = action_dict.get(TFields.PROPERTIES, {})
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)
actions.append(ActionSpecs(action_type, targets, properties))
def __eq__(self, other):
return self.scenario_id == other.scenario_id \
and self.condition == other.condition \
and self.actions == other.actions
return 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)
def _build_subgraphs(self, condition):
return [self._build_subgraph(clause) for clause in condition]
@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 _build_subgraph(self, clause):
condition_g = NXGraph("scenario condition")
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))
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)
subgraphs = TemplateData.SubGraph.from_condition(
scenario.condition, extract_var)
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 Scenario(id=scenario.id + '_equivalence',
condition=scenario.condition,
actions=scenario.actions,
subgraphs=subgraphs,
entities=entities,
relationships=relationships)
return condition_g
@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 _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
@staticmethod
def _build_actions(actions_def):
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
actions = []
for action_def in actions_def:
@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)
action_dict = action_def[TFields.ACTION]
action_type = action_dict[TFields.ACTION_TYPE]
targets = action_dict[TFields.ACTION_TARGET]
properties = action_dict.get(TFields.PROPERTIES, {})
def _parse_condition(self, condition_str):
"""Parse condition string into an object
actions.append(ActionSpecs(action_type, targets, properties))
The condition string will be converted here into DNF (Disjunctive
Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)...
where X, Y, Z, V, W are either entities or relationships
more details: https://en.wikipedia.org/wiki/Disjunctive_normal_form
return actions
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
def _parse_condition(self, condition_str):
"""Parse condition string into an object
[[and_var1, and_var2, ...], or_list_2, ...]
The condition string will be converted here into DNF (Disjunctive
Normal Form), e.g., (X and Y) or (X and Z) or (X and V and not W)
... where X, Y, Z, V, W are either entities or relationships
more details: https://en.wikipedia.org/wiki/Disjunctive_normal_form
:param condition_str: the string as it written in the template itself
:return: condition_vars_lists
"""
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
condition_dnf = self.convert_to_dnf_format(condition_str)
[[and_var1, and_var2, ...], or_list_2, ...]
if isinstance(condition_dnf, Or):
return self._extract_or_condition(condition_dnf)
:param condition_str: the string as it written in the template
:return: condition_vars_lists
"""
if isinstance(condition_dnf, And):
return [self._extract_and_condition(condition_dnf)]
condition_dnf = self.convert_to_dnf_format(condition_str)
if isinstance(condition_dnf, Not):
return [(self._extract_not_condition_var(condition_dnf))]
if isinstance(condition_dnf, Or):
return self._extract_or_condition(condition_dnf)
if isinstance(condition_dnf, Symbol):
return [[(self._extract_condition_var(condition_dnf, True))]]
if isinstance(condition_dnf, And):
return [self._extract_and_condition(condition_dnf)]
@staticmethod
def convert_to_dnf_format(condition_str):
if isinstance(condition_dnf, Not):
return [(self._extract_not_condition_var(condition_dnf))]
condition_str = condition_str.replace(' and ', '&')
condition_str = condition_str.replace(' or ', '|')
condition_str = condition_str.replace(' not ', '~')
condition_str = condition_str.replace('not ', '~')
if isinstance(condition_dnf, Symbol):
return [[(self._extract_condition_var(condition_dnf, True))]]
return sympy_to_dfn(condition_str)
@staticmethod
def convert_to_dnf_format(condition_str):
def _extract_or_condition(self, or_condition):
condition_str = condition_str.replace(' and ', '&')
condition_str = condition_str.replace(' or ', '|')
condition_str = condition_str.replace(' not ', '~')
condition_str = condition_str.replace('not ', '~')
vars_ = []
for var in or_condition.args:
return sympy_to_dfn(condition_str)
if isinstance(var, And):
vars_.append(self._extract_and_condition(var))
def _extract_or_condition(self, or_condition):
vars_ = []
for var in or_condition.args:
if isinstance(var, And):
vars_.append(self._extract_and_condition(var))
else:
is_symbol = isinstance(var, Symbol)
vars_.append([self._extract_condition_var(var, is_symbol)])
return vars_
def _extract_and_condition(self, and_condition):
return [self._extract_condition_var(arg, isinstance(arg, Symbol))
for arg in and_condition.args]
def _extract_not_condition_var(self, not_condition):
return [self._extract_condition_var(arg, False)
for arg in not_condition.args]
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:
is_symbol = isinstance(var, Symbol)
vars_.append([self._extract_condition_var(var, is_symbol)])
edge_description.edge[EProps.IS_DELETED] = False
edge_description.edge[NEG_CONDITION] = False
return vars_
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
def _extract_and_condition(self, and_condition):
return [self._extract_condition_var(arg, isinstance(arg, Symbol))
for arg in and_condition.args]
def _extract_not_condition_var(self, not_condition):
return [self._extract_condition_var(arg, False)
for arg in not_condition.args]
def _extract_condition_var(self, symbol, positive):
if isinstance(symbol, Not):
return self._extract_not_condition_var(symbol)[0]
else:
var, var_type = self._extract_var(str(symbol))
return ConditionVar(var, var_type, positive)
def _extract_var(self, template_id):
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)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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