Merge "Support template functions"

This commit is contained in:
Zuul 2018-01-17 16:40:07 +00:00 committed by Gerrit Code Review
commit c350c92482
35 changed files with 544 additions and 36 deletions

View File

@ -128,11 +128,17 @@ as high availability condition.
Scenarios Scenarios
========= =========
``Scenario`` are defined as a ``namedtuple`` ``Scenario`` class holds the following properties:
.. code-block:: python * id
* version
* condition
* actions
* subgraphs
* entities
* relationships
* enabled
Scenario = namedtuple('Scenario', ['id', 'condition', 'actions', 'subgraphs'])
id id
-- --

View File

@ -406,6 +406,40 @@ regular expression:
rawtext.regex: Interface ([_a-zA-Z0-9'-]+) down on {HOST.NAME} rawtext.regex: Interface ([_a-zA-Z0-9'-]+) down on {HOST.NAME}
template_id: zabbix_alarm template_id: zabbix_alarm
Using functions in an action definition
---------------------------------------
Some properties of an action can be defined using functions. On version 2, one
function is supported: get_attr, and it is supported only for execute_mistral
action.
*Note:* Functions are supported from version 2 and on.
get_attr
^^^^^^^^
This function retrieves the value of an attribute of an entity that is defined
in the template.
Usage
~~~~~
get_attr(template_id, attr_name)
Example
~~~~~~~
::
scenario:
condition: alarm_on_host_1
actions:
action:
action_type: execute_mistral
properties:
workflow: demo_workflow
input:
host_name: get_attr(host_1,name)
retries: 5
Supported Actions Supported Actions
----------------- -----------------

View File

@ -0,0 +1,6 @@
---
features:
- Support functions in Vitrage templates version 2. The first supported
function is ``get_attr`` which allows retrieving attributes from the
matched entity in the graph. As the first stage this function is supported
only for ``execute_mistral`` action.

View File

@ -12,5 +12,15 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
from collections import namedtuple from collections import namedtuple
import re
Template = namedtuple('Template', ['uuid', 'data', 'date', 'result']) Template = namedtuple('Template', ['uuid', 'data', 'date', 'result'])
def is_function(str):
"""Check if the string represents a function
A function has the format: func_name(params)
Search for a regex with open and close parenthesis
"""
return re.match('.*\(.*\)', str)

View File

@ -14,6 +14,7 @@
from collections import namedtuple from collections import namedtuple
from collections import OrderedDict from collections import OrderedDict
import copy import copy
import re
import time import time
from oslo_log import log from oslo_log import log
@ -28,8 +29,10 @@ from vitrage.evaluator.actions.action_executor import ActionExecutor
from vitrage.evaluator.actions.base import ActionMode from vitrage.evaluator.actions.base import ActionMode
from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.actions.base import ActionType
import vitrage.evaluator.actions.priority_tools as pt import vitrage.evaluator.actions.priority_tools as pt
from vitrage.evaluator.base import is_function
from vitrage.evaluator.template_data import ActionSpecs from vitrage.evaluator.template_data import ActionSpecs
from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_data import EdgeDescription
from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory
from vitrage.graph.algo_driver.algorithm import Mapping from vitrage.graph.algo_driver.algorithm import Mapping
from vitrage.graph.algo_driver.sub_graph_matching import \ from vitrage.graph.algo_driver.sub_graph_matching import \
NEG_CONDITION NEG_CONDITION
@ -184,7 +187,8 @@ class ScenarioEvaluator(object):
scenario_element, scenario_element,
action.targets[TARGET]) action.targets[TARGET])
actions.extend(self._get_actions_from_matches(matches, actions.extend(self._get_actions_from_matches(scenario.version,
matches,
mode, mode,
action)) action))
@ -207,6 +211,7 @@ class ScenarioEvaluator(object):
scenario_element) scenario_element)
def _get_actions_from_matches(self, def _get_actions_from_matches(self,
scenario_version,
combined_matches, combined_matches,
mode, mode,
action_spec): action_spec):
@ -217,15 +222,67 @@ class ScenarioEvaluator(object):
new_mode = ActionMode.UNDO \ new_mode = ActionMode.UNDO \
if mode == ActionMode.DO else ActionMode.DO if mode == ActionMode.DO else ActionMode.DO
template_schema = \
TemplateSchemaFactory().template_schema(scenario_version)
for match in matches: for match in matches:
match_action_spec = self._get_action_spec(action_spec, match) match_action_spec = self._get_action_spec(action_spec, match)
items_ids = [match[1].vertex_id for match in match.items()] items_ids = \
[match_item[1].vertex_id for match_item in match.items()]
match_hash = hash(tuple(sorted(items_ids))) match_hash = hash(tuple(sorted(items_ids)))
self._evaluate_property_functions(template_schema, match,
match_action_spec.properties)
actions.append(ActionInfo(match_action_spec, new_mode, actions.append(ActionInfo(match_action_spec, new_mode,
match_action_spec.id, match_hash)) match_action_spec.id, match_hash))
return actions return actions
def _evaluate_property_functions(self, template_schema, match,
action_props):
"""Evaluate the action properties, in case they contain functions
In template version 2 we introduced functions, and specifically the
get_attr function. This method evaluate its value and updates the
action properties, before the action is being executed.
Example:
- action:
action_type: execute_mistral
properties:
workflow: evacuate_vm
input:
vm_name: get_attr(instance1,name)
force: false
In this example, the method will iterate over 'properties', and then
recursively over 'input', and for 'vm_name' it will replace the
call for get_attr with the actual name of the VM. The input for the
Mistral workflow will then be:
vm_name: vm_1
force: false
"""
for key, value in action_props.items():
if isinstance(value, dict):
# Recursive call for a dictionary
self._evaluate_property_functions(template_schema,
match, value)
elif value is not None and is_function(value):
# The value is a function
func_and_args = re.split('[(),]', value)
func_name = func_and_args.pop(0)
args = [arg.strip() for arg in func_and_args if len(arg) > 0]
# Get the function, execute it and update the property value
func = template_schema.functions.get(func_name)
action_props[key] = func(match, *args)
LOG.debug('Changed property %s value from %s to %s', key,
value, action_props[key])
@staticmethod @staticmethod
def _get_action_spec(action_spec, match): def _get_action_spec(action_spec, match):
targets = action_spec.targets targets = action_spec.targets

View File

@ -145,7 +145,7 @@ class ScenarioRepository(object):
if result.is_valid_config: if result.is_valid_config:
def_validator = \ def_validator = \
template_schema.validator(TemplateFields.DEFINITIONS) template_schema.validators.get(TemplateFields.DEFINITIONS)
result = \ result = \
def_validator.def_template_content_validation(def_template) def_validator.def_template_content_validation(def_template)

View File

@ -24,9 +24,10 @@ RELATIONSHIP = 'relationship'
class Scenario(object): class Scenario(object):
def __init__(self, id, condition, actions, subgraphs, entities, def __init__(self, id, version, condition, actions, subgraphs, entities,
relationships, enabled=False): relationships, enabled=False):
self.id = id self.id = id
self.version = version
self.condition = condition self.condition = condition
self.actions = actions self.actions = actions
self.subgraphs = subgraphs self.subgraphs = subgraphs
@ -46,8 +47,9 @@ class Scenario(object):
# noinspection PyAttributeOutsideInit # noinspection PyAttributeOutsideInit
class TemplateData(object): class TemplateData(object):
def __init__(self, name, entities, relationships, scenarios): def __init__(self, name, version, entities, relationships, scenarios):
self.name = name self.name = name
self.version = version
self.entities = entities self.entities = entities
self.relationships = relationships self.relationships = relationships
self.scenarios = scenarios self.scenarios = scenarios
@ -60,6 +62,14 @@ class TemplateData(object):
def name(self, template_name): def name(self, template_name):
self._name = template_name self._name = template_name
@property
def version(self):
return self._version
@version.setter
def version(self, version):
self._version = version
@property @property
def entities(self): def entities(self):
return self._entities return self._entities

View File

@ -0,0 +1,15 @@
# Copyright 2018 - 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.
__author__ = 'stack'

View File

@ -0,0 +1,15 @@
# Copyright 2018 - 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.
__author__ = 'stack'

View File

@ -0,0 +1,78 @@
# Copyright 2018 - Nokia
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log
LOG = log.getLogger(__name__)
# Function names
GET_ATTR = 'get_attr'
def get_attr(match, *args):
"""Get the runtime value of an attribute of a template entity
Usage: get_attr(template_id, attr_name)
Example:
scenario:
condition: alarm_on_host_1
actions:
action:
action_type: execute_mistral
properties:
workflow: demo_workflow
input:
host_name: get_attr(host_1,name)
retries: 5
get_attr(host_1, name) will return the name of the host that was matched
by the evaluator to host_1
:param match: The evaluator's match structure. A dictionary of
{template_id, Vertex}
:param args: The arguments of the function. For get_attr, the expected
arguments are:
- template_id: The internal template id of the entity
- attr_name: The name of the wanted attribute
:return: The wanted attribute if found, or None
"""
if len(args) != 2:
LOG.warning('Called function get_attr with wrong number of '
'arguments: %s. Usage: get_attr(vertex, attr_name)',
args)
return
template_id = args[0]
attr_name = args[1]
vertex = match.get(template_id)
if not vertex:
LOG.warning('Called function get_attr with unknown template_id %s',
args[0])
return
entity_props = vertex.properties
attr = entity_props.get(attr_name) if entity_props else None
if attr is None:
LOG.warning('Attribute %s not found for vertex %s',
attr_name, str(vertex))
LOG.debug('Function get_attr called with template_id %s and attr_name %s.'
'Matched vertex properties: %s. Returned attribute value: %s',
template_id, attr_name, str(entity_props), attr)
return attr

View File

@ -57,7 +57,8 @@ class ScenarioLoader(object):
self._extract_var_and_update_index) self._extract_var_and_update_index)
scenarios.append( scenarios.append(
Scenario(scenario_id, condition, actions, subgraphs, Scenario(scenario_id, self._template_schema.version(),
condition, actions, subgraphs,
self.entities, self.relationships)) self.entities, self.relationships))
return scenarios return scenarios
@ -87,6 +88,7 @@ class ScenarioLoader(object):
scenario.condition, extract_var) scenario.condition, extract_var)
return Scenario(id=scenario.id + '_equivalence', return Scenario(id=scenario.id + '_equivalence',
version=scenario.version,
condition=scenario.condition, condition=scenario.condition,
actions=scenario.actions, actions=scenario.actions,
subgraphs=subgraphs, subgraphs=subgraphs,
@ -99,7 +101,7 @@ class ScenarioLoader(object):
for counter, action_def in enumerate(actions_def): for counter, action_def in enumerate(actions_def):
action_id = '%s-action%s' % (scenario_id, str(counter)) action_id = '%s-action%s' % (scenario_id, str(counter))
action_type = action_def[TFields.ACTION][TFields.ACTION_TYPE] action_type = action_def[TFields.ACTION][TFields.ACTION_TYPE]
action_loader = self._template_schema.loader(action_type) action_loader = self._template_schema.loaders.get(action_type)
if action_loader: if action_loader:
actions.append(action_loader.load(action_id, self.valid_target, actions.append(action_loader.load(action_id, self.valid_target,

View File

@ -86,7 +86,8 @@ class TemplateLoader(object):
self.relationships).\ self.relationships).\
build_scenarios(template_def[TFields.SCENARIOS]) build_scenarios(template_def[TFields.SCENARIOS])
return TemplateData(name, self.entities, self.relationships, scenarios) return TemplateData(name, template_schema.version(), self.entities,
self.relationships, scenarios)
def _build_entities(self, entities_defs): def _build_entities(self, entities_defs):
entities = {} entities = {}

View File

@ -15,6 +15,8 @@ from oslo_log import log
from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.actions.base import ActionType
from vitrage.evaluator.template_fields import TemplateFields 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.v1.action_loader import ActionLoader from vitrage.evaluator.template_loading.v1.action_loader import ActionLoader
from vitrage.evaluator.template_loading.v1.execute_mistral_loader import \ from vitrage.evaluator.template_loading.v1.execute_mistral_loader import \
ExecuteMistralLoader ExecuteMistralLoader
@ -43,7 +45,7 @@ LOG = log.getLogger(__name__)
class TemplateSchema1(object): class TemplateSchema1(object):
def __init__(self): def __init__(self):
self._validators = { self.validators = {
TemplateFields.DEFINITIONS: DefinitionsValidator, TemplateFields.DEFINITIONS: DefinitionsValidator,
TemplateFields.SCENARIOS: ScenarioValidator, TemplateFields.SCENARIOS: ScenarioValidator,
ActionType.ADD_CAUSAL_RELATIONSHIP: AddCausalRelationshipValidator, ActionType.ADD_CAUSAL_RELATIONSHIP: AddCausalRelationshipValidator,
@ -53,7 +55,7 @@ class TemplateSchema1(object):
ActionType.SET_STATE: SetStateValidator, ActionType.SET_STATE: SetStateValidator,
} }
self._loaders = { self.loaders = {
ActionType.ADD_CAUSAL_RELATIONSHIP: ActionLoader(), ActionType.ADD_CAUSAL_RELATIONSHIP: ActionLoader(),
ActionType.EXECUTE_MISTRAL: ExecuteMistralLoader(), ActionType.EXECUTE_MISTRAL: ExecuteMistralLoader(),
ActionType.MARK_DOWN: ActionLoader(), ActionType.MARK_DOWN: ActionLoader(),
@ -61,24 +63,23 @@ class TemplateSchema1(object):
ActionType.SET_STATE: ActionLoader(), ActionType.SET_STATE: ActionLoader(),
} }
def validator(self, validator_type): self.functions = {}
LOG.debug('Get validator. validator_type: %s. validators: %s',
validator_type, self._validators)
return self._validators.get(validator_type)
def loader(self, loader_type): def version(self):
LOG.debug('Get loader. loader_type: %s. loaders: %s', return '1'
loader_type, self._loaders)
return self._loaders.get(loader_type)
class TemplateSchema2(TemplateSchema1): class TemplateSchema2(TemplateSchema1):
def __init__(self): def __init__(self):
super(TemplateSchema2, self).__init__() super(TemplateSchema2, self).__init__()
self._validators[ActionType.EXECUTE_MISTRAL] = \ self.validators[ActionType.EXECUTE_MISTRAL] = \
V2ExecuteMistralValidator() V2ExecuteMistralValidator()
self._loaders[ActionType.EXECUTE_MISTRAL] = ActionLoader() self.loaders[ActionType.EXECUTE_MISTRAL] = ActionLoader()
self.functions[GET_ATTR] = get_attr
def version(self):
return '2'
def init_template_schemas(): def init_template_schemas():

View File

@ -22,6 +22,10 @@ def get_correct_result(description):
return Result(description, True, 0, status_msgs[0]) return Result(description, True, 0, status_msgs[0])
def get_warning_result(description, code):
return Result(description, True, code, status_msgs[code])
def get_fault_result(description, code, msg=None): def get_fault_result(description, code, msg=None):
if msg: if msg:
return Result(description, False, code, msg) return Result(description, False, code, msg)

View File

@ -20,6 +20,7 @@ from vitrage.evaluator.template_fields import TemplateFields
from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory
from vitrage.evaluator.template_validation.base import get_correct_result 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_fault_result
from vitrage.evaluator.template_validation.base import get_warning_result
from vitrage.evaluator.template_validation.status_messages import status_msgs from vitrage.evaluator.template_validation.status_messages import status_msgs
@ -35,6 +36,10 @@ def get_content_fault_result(code, msg=None):
return get_fault_result(RESULT_DESCRIPTION, code, msg) return get_fault_result(RESULT_DESCRIPTION, code, msg)
def get_content_warning_result(code):
return get_warning_result(RESULT_DESCRIPTION, code)
def validate_template_id(definitions_index, id_to_check): def validate_template_id(definitions_index, id_to_check):
if id_to_check not in definitions_index: if id_to_check not in definitions_index:
msg = status_msgs[3] + ' template id: %s' % id_to_check msg = status_msgs[3] + ' template id: %s' % id_to_check

View File

@ -30,7 +30,8 @@ def content_validation(template, def_templates=None):
template_definitions = {} template_definitions = {}
result, template_schema = get_template_schema(template) result, template_schema = get_template_schema(template)
def_validator = template_schema.validator(TemplateFields.DEFINITIONS) \ def_validator = \
template_schema.validators.get(TemplateFields.DEFINITIONS) \
if result.is_valid_config and template_schema else None if result.is_valid_config and template_schema else None
if result.is_valid_config and not def_validator: if result.is_valid_config and not def_validator:
@ -71,7 +72,7 @@ def content_validation(template, def_templates=None):
relationship_index) relationship_index)
if result.is_valid_config: if result.is_valid_config:
scenario_validator = template_schema.validator( scenario_validator = template_schema.validators.get(
TemplateFields.SCENARIOS) TemplateFields.SCENARIOS)
scenarios = template[TemplateFields.SCENARIOS] scenarios = template[TemplateFields.SCENARIOS]
definitions_index = entities_index.copy() definitions_index = entities_index.copy()

View File

@ -15,6 +15,7 @@
from oslo_log import log from oslo_log import log
from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW
from vitrage.evaluator.base import is_function
from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_fields import TemplateFields
from vitrage.evaluator.template_validation.content.base import \ from vitrage.evaluator.template_validation.content.base import \
ActionValidator ActionValidator
@ -38,4 +39,9 @@ class ExecuteMistralValidator(ActionValidator):
LOG.error('%s status code: %s' % (status_msgs[133], 133)) LOG.error('%s status code: %s' % (status_msgs[133], 133))
return get_content_fault_result(133) return get_content_fault_result(133)
for key, value in properties.items():
if not isinstance(value, dict) and is_function(value):
LOG.error('%s status code: %s' % (status_msgs[137], 137))
return get_content_fault_result(137)
return get_content_correct_result() return get_content_correct_result()

View File

@ -184,7 +184,7 @@ class ScenarioValidator(object):
@staticmethod @staticmethod
def _validate_scenario_action(template_schema, def_index, action): def _validate_scenario_action(template_schema, def_index, action):
action_type = action[TemplateFields.ACTION_TYPE] action_type = action[TemplateFields.ACTION_TYPE]
action_validator = template_schema.validator(action_type) action_validator = template_schema.validators.get(action_type)
if not action_validator: if not action_validator:
LOG.error('%s status code: %s' % (status_msgs[120], 120)) LOG.error('%s status code: %s' % (status_msgs[120], 120))

View File

@ -13,9 +13,11 @@
# under the License. # under the License.
from oslo_log import log from oslo_log import log
import re
from vitrage.evaluator.actions.recipes.execute_mistral import INPUT from vitrage.evaluator.actions.recipes.execute_mistral import INPUT
from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW
from vitrage.evaluator.base import is_function
from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_fields import TemplateFields
from vitrage.evaluator.template_validation.content.base import \ from vitrage.evaluator.template_validation.content.base import \
ActionValidator ActionValidator
@ -23,6 +25,8 @@ from vitrage.evaluator.template_validation.content.base import \
get_content_correct_result get_content_correct_result
from vitrage.evaluator.template_validation.content.base import \ from vitrage.evaluator.template_validation.content.base import \
get_content_fault_result get_content_fault_result
from vitrage.evaluator.template_validation.content.base import \
get_content_warning_result
from vitrage.evaluator.template_validation.status_messages import status_msgs from vitrage.evaluator.template_validation.status_messages import status_msgs
@ -44,4 +48,11 @@ class ExecuteMistralValidator(ActionValidator):
LOG.error('%s status code: %s' % (status_msgs[136], 136)) LOG.error('%s status code: %s' % (status_msgs[136], 136))
return get_content_fault_result(136) return get_content_fault_result(136)
inputs = properties[INPUT] if INPUT in properties else {}
for key, value in inputs.items():
if re.findall('[(),]', value) and not is_function(value):
LOG.error('%s status code: %s' % (status_msgs[138], 138))
return get_content_warning_result(138)
return get_content_correct_result() return get_content_correct_result()

View File

@ -84,6 +84,9 @@ status_msgs = {
135: 'condition must contain a common entity for all \'or\' clauses', 135: 'condition must contain a common entity for all \'or\' clauses',
136: 'Input parameters for the Mistral workflow in execute_mistral action ' 136: 'Input parameters for the Mistral workflow in execute_mistral action '
'must be placed under an \'input\' block ', '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 '
'a function?',
# def_templates status messages 140-159 # def_templates status messages 140-159
140: 'At least one template must be included', 140: 'At least one template must be included',

View File

@ -0,0 +1,30 @@
metadata:
version: 2
name: v2_with_func
description: template with a function
definitions:
entities:
- entity:
category: ALARM
name: notifiers.mistral.trigger.alarm.for.function
template_id: alarm
- entity:
category: RESOURCE
type: nova.host
template_id: host
relationships:
- relationship:
source: alarm
relationship_type: on
target: host
template_id : alarm_on_host
scenarios:
- scenario:
condition: alarm_on_host
actions:
- action:
action_type: execute_mistral
properties:
workflow: wf_for_tempest_test_1234
input:
farewell: get_attr(alarm,name)

View File

@ -0,0 +1,15 @@
# Copyright 2018 - 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.
__author__ = 'stack'

View File

@ -0,0 +1,59 @@
# Copyright 2018 - 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_functions.v2.functions import get_attr
from vitrage.graph.driver import Vertex
from vitrage.tests import base
class TemplateFunctionsTest(base.BaseTest):
def test_get_attr_with_existing_attr(self):
entity_id = 'id1234'
match = self._create_match('instance', properties={'id': entity_id})
attr = get_attr(match, 'instance', 'id')
self.assertIsNotNone(attr)
self.assertEqual(entity_id, attr)
def test_get_attr_with_non_existing_attr(self):
match = self._create_match('instance', properties={'id': 'id1'})
attr = get_attr(match, 'instance', 'non_existing_attr')
self.assertIsNone(attr)
def test_get_attr_with_two_attrs(self):
properties = {'attr1': 'first_attr', 'attr2': 'second_attr'}
match = self._create_match('instance', properties)
attr = get_attr(match, 'instance', 'attr1')
self.assertIsNotNone(attr)
self.assertEqual('first_attr', attr)
attr = get_attr(match, 'instance', 'attr2')
self.assertIsNotNone(attr)
self.assertEqual('second_attr', attr)
attr = get_attr(match, 'instance', 'attr3')
self.assertIsNone(attr)
def test_get_attr_with_non_existing_entity(self):
match = self._create_match('instance', properties={'attr1': 'attr1'})
attr = get_attr(match, 'non_existing_entity', 'attr1')
self.assertIsNone(attr)
@staticmethod
def _create_match(template_id, properties):
entity = Vertex(vertex_id='f89fe840-b595-4010-8a09-a444c7642865',
properties=properties)
return {template_id: entity}

View File

@ -47,6 +47,12 @@ class ValidatorTest(base.BaseTest):
self.assertTrue(result.comment.startswith(status_msgs[status_code])) self.assertTrue(result.comment.startswith(status_msgs[status_code]))
self.assertEqual(result.status_code, status_code) self.assertEqual(result.status_code, status_code)
def _assert_warning_result(self, result, status_code):
self.assertTrue(result.is_valid_config)
self.assertTrue(result.comment.startswith(status_msgs[status_code]))
self.assertEqual(result.status_code, status_code)
@staticmethod @staticmethod
def _hide_useless_logging_messages(): def _hide_useless_logging_messages():

View File

@ -98,13 +98,16 @@ class BaseExecuteMistralValidatorTest(ActionValidatorTest):
return action return action
@staticmethod @staticmethod
def _create_v1_execute_mistral_action(workflow, host, host_state): def _create_v1_execute_mistral_action(workflow, host, host_state,
**kwargs):
properties = { properties = {
WORKFLOW: workflow, WORKFLOW: workflow,
'host': host, 'host': host,
'host_state': host_state 'host_state': host_state
} }
properties.update(kwargs)
action = { action = {
TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL, TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL,
TemplateFields.PROPERTIES: properties TemplateFields.PROPERTIES: properties
@ -113,16 +116,19 @@ class BaseExecuteMistralValidatorTest(ActionValidatorTest):
return action return action
@staticmethod @staticmethod
def _create_v2_execute_mistral_action(workflow, host, host_state): def _create_v2_execute_mistral_action(workflow, host, host_state,
**kwargs):
input_props = { input_props = {
'host': host, 'host': host,
'host_state': host_state 'host_state': host_state
} }
input_props.update(kwargs)
properties = { properties = {
WORKFLOW: workflow, WORKFLOW: workflow,
'input': input_props 'input': input_props
} }
action = { action = {
TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL, TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL,
TemplateFields.PROPERTIES: properties TemplateFields.PROPERTIES: properties

View File

@ -289,7 +289,7 @@ class TemplateContentValidatorTest(ValidatorTest):
def test_validate_template_with_version_1(self): def test_validate_template_with_version_1(self):
invalid_version_path = \ invalid_version_path = \
VERSION_TEMPLATE_DIR % (utils.get_resources_dir(), VERSION_TEMPLATE_DIR % (utils.get_resources_dir(),
"version1.yaml") "v1/version1.yaml")
template = file_utils.load_yaml_file(invalid_version_path) template = file_utils.load_yaml_file(invalid_version_path)
self._execute_and_assert_with_correct_result(template) self._execute_and_assert_with_correct_result(template)

View File

@ -60,6 +60,34 @@ class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest):
# Test assertions # Test assertions
self._assert_correct_result(result) self._assert_correct_result(result)
def test_validate_execute_mistral_action_with_input_dict(self):
"""A version1 execute_mistral action can have an 'input' dictionary"""
# Test setup
idx = DEFINITIONS_INDEX_MOCK.copy()
action = self._create_execute_mistral_action('wf_1', 'host_2', 'down')
input_dict = {'a': '1'}
action[TemplateFields.PROPERTIES]['input'] = input_dict
# Test action
result = self.validator.validate(action, idx)
# Test assertions
self._assert_correct_result(result)
def test_validate_execute_mistral_action_with_func(self):
# Test setup
idx = DEFINITIONS_INDEX_MOCK.copy()
action = \
self._create_v1_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr(alarm, name)')
# Test action
result = self.validator.validate(action, idx)
# Test assertions
self._assert_fault_result(result, 137)
def _create_execute_mistral_action(self, workflow, host, host_state): def _create_execute_mistral_action(self, workflow, host, host_state):
return self.\ return self.\
_create_v1_execute_mistral_action(workflow, host, host_state) _create_v1_execute_mistral_action(workflow, host, host_state)

View File

@ -63,6 +63,53 @@ class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest):
self._validate_execute_mistral_action_without_additional_props( self._validate_execute_mistral_action_without_additional_props(
self.validator) self.validator)
def test_v2_validate_execute_mistral_action_with_func(self):
self._validate_action(
self._create_v2_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr(alarm,name)'),
self.validator.validate
)
def test_v2_validate_execute_mistral_action_with_func_2(self):
self._validate_action(
self._create_v2_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr(alarm, name)'),
self.validator.validate
)
def test_v2_validate_execute_mistral_action_with_func_3(self):
self._validate_action(
self._create_v2_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr ( alarm , name ) '),
self.validator.validate
)
def test_v2_validate_execute_mistral_action_with_func_typo_1(self):
# Test setup
idx = DEFINITIONS_INDEX_MOCK.copy()
action = \
self._create_v2_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr(alarm, name')
# Test action
result = self.validator.validate(action, idx)
# Test assertions
self._assert_warning_result(result, 138)
def test_v2_validate_execute_mistral_action_with_func_typo_2(self):
# Test setup
idx = DEFINITIONS_INDEX_MOCK.copy()
action = \
self._create_v2_execute_mistral_action(
'wf_1', 'host_2', 'down', func1='get_attr, name)')
# Test action
result = self.validator.validate(action, idx)
# Test assertions
self._assert_warning_result(result, 138)
def _create_execute_mistral_action(self, workflow, host, host_state): def _create_execute_mistral_action(self, workflow, host, host_state):
return self.\ return self.\
_create_v2_execute_mistral_action(workflow, host, host_state) _create_v2_execute_mistral_action(workflow, host, host_state)

View File

@ -228,7 +228,7 @@ class TemplateSyntaxValidatorTest(base.BaseTest):
self._test_execution_with_correct_result(template) self._test_execution_with_correct_result(template)
def test_template_with_valid_version(self): def test_template_with_valid_version(self):
template_path = self.version_dir_path + 'version1.yaml' template_path = self.version_dir_path + 'v1/version1.yaml'
template = file_utils.load_yaml_file(template_path) template = file_utils.load_yaml_file(template_path)
self._test_execution_with_correct_result(template) self._test_execution_with_correct_result(template)

View File

@ -38,8 +38,8 @@ class BasicTemplateTest(base.BaseTest):
BASIC_TEMPLATE = 'basic.yaml' BASIC_TEMPLATE = 'basic.yaml'
BASIC_TEMPLATE_WITH_INCLUDE = 'basic_with_include.yaml' BASIC_TEMPLATE_WITH_INCLUDE = 'basic_with_include.yaml'
V1_MISTRAL_TEMPLATE = 'v1_execute_mistral.yaml' V1_MISTRAL_TEMPLATE = 'v1/v1_execute_mistral.yaml'
V2_MISTRAL_TEMPLATE = 'v2_execute_mistral.yaml' V2_MISTRAL_TEMPLATE = 'v2/v2_execute_mistral.yaml'
DEF_TEMPLATE_TESTS_DIR = utils.get_resources_dir() +\ DEF_TEMPLATE_TESTS_DIR = utils.get_resources_dir() +\
'/templates/def_template_tests' '/templates/def_template_tests'
@ -135,6 +135,7 @@ class BasicTemplateTest(base.BaseTest):
expected_scenario = Scenario( expected_scenario = Scenario(
id='basic_template_with_include-scenario0', id='basic_template_with_include-scenario0',
version=1,
condition=[ condition=[
[ConditionVar(symbol_name='alarm_on_host', [ConditionVar(symbol_name='alarm_on_host',
positive=True)]], positive=True)]],
@ -209,6 +210,7 @@ class BasicTemplateTest(base.BaseTest):
expected_scenario = Scenario( expected_scenario = Scenario(
id='basic_template-scenario0', id='basic_template-scenario0',
version=1,
condition=[ condition=[
[ConditionVar(symbol_name='alarm_on_host', [ConditionVar(symbol_name='alarm_on_host',
positive=True)]], positive=True)]],

View File

@ -12,6 +12,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import json
from oslo_log import log as logging from oslo_log import log as logging
from testtools.matchers import HasLength from testtools.matchers import HasLength
@ -45,6 +46,7 @@ class TestMistralNotifier(BaseTestEvents):
TRIGGER_ALARM_1 = "notifiers.mistral.trigger.alarm.1" TRIGGER_ALARM_1 = "notifiers.mistral.trigger.alarm.1"
TRIGGER_ALARM_2 = "notifiers.mistral.trigger.alarm.2" TRIGGER_ALARM_2 = "notifiers.mistral.trigger.alarm.2"
TRIGGER_ALARM_FOR_FUNCTION = "notifiers.mistral.trigger.alarm.for.function"
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@ -59,6 +61,36 @@ class TestMistralNotifier(BaseTestEvents):
def test_execute_mistral_v2(self): def test_execute_mistral_v2(self):
self._do_test_execute_mistral(self.TRIGGER_ALARM_2) self._do_test_execute_mistral(self.TRIGGER_ALARM_2)
@utils.tempest_logger
def test_execute_mistral_with_function(self):
# Execute the basic test
self._do_test_execute_mistral(self.TRIGGER_ALARM_FOR_FUNCTION)
# Make sure that the workflow execution was done with the correct input
# (can be checked even if the Vitrage alarm is already down)
executions = self.mistral_client.executions.list()
last_execution = executions[0]
for execution in executions:
if execution.updated_at > last_execution.updated_at:
last_execution = execution
execution_input_str = last_execution.input
self.assertIsNotNone(execution_input_str,
'The last execution had no input')
self.assertIn('farewell', execution_input_str,
'No \'farewell\' key in the last execution input')
execution_input = json.loads(execution_input_str)
farewell_value = execution_input['farewell']
self.assertIsNotNone(farewell_value, '\'farewell\' input parameter is '
'None in last workflow execution')
self.assertEqual(self.TRIGGER_ALARM_FOR_FUNCTION, farewell_value,
'\'farewell\' input parameter does not match the'
'alarm name')
def _do_test_execute_mistral(self, trigger_alarm): def _do_test_execute_mistral(self, trigger_alarm):
workflows = self.mistral_client.workflows.list() workflows = self.mistral_client.workflows.list()
self.assertIsNotNone(workflows, 'Failed to get the list of workflows') self.assertIsNotNone(workflows, 'Failed to get the list of workflows')

View File

@ -7,20 +7,29 @@ definitions:
- entity: - entity:
category: ALARM category: ALARM
name: notifiers.mistral.trigger.alarm.2 name: notifiers.mistral.trigger.alarm.2
template_id: alarm template_id: alarm_2
- entity:
category: ALARM
name: notifiers.mistral.trigger.alarm.for.function
template_id: alarm_for_func
- entity: - entity:
category: RESOURCE category: RESOURCE
type: nova.host type: nova.host
template_id: host template_id: host
relationships: relationships:
- relationship: - relationship:
source: alarm source: alarm_2
relationship_type: on relationship_type: on
target: host target: host
template_id : alarm_on_host template_id : alarm_2_on_host
- relationship:
source: alarm_for_func
relationship_type: on
target: host
template_id : alarm_for_func_on_host
scenarios: scenarios:
- scenario: - scenario:
condition: alarm_on_host condition: alarm_2_on_host
actions: actions:
- action: - action:
action_type: execute_mistral action_type: execute_mistral
@ -28,3 +37,12 @@ scenarios:
workflow: wf_for_tempest_test_1234 workflow: wf_for_tempest_test_1234
input: input:
farewell: Hello and Goodbye farewell: Hello and Goodbye
- scenario:
condition: alarm_for_func_on_host
actions:
- action:
action_type: execute_mistral
properties:
workflow: wf_for_tempest_test_1234
input:
farewell: get_attr(alarm_for_func,name)