Merge "Support template functions"
This commit is contained in:
commit
c350c92482
@ -128,11 +128,17 @@ as high availability condition.
|
||||
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
|
||||
--
|
||||
|
@ -406,6 +406,40 @@ regular expression:
|
||||
rawtext.regex: Interface ([_a-zA-Z0-9'-]+) down on {HOST.NAME}
|
||||
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
|
||||
-----------------
|
||||
|
||||
|
@ -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.
|
@ -12,5 +12,15 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
from collections import namedtuple
|
||||
import re
|
||||
|
||||
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)
|
||||
|
@ -14,6 +14,7 @@
|
||||
from collections import namedtuple
|
||||
from collections import OrderedDict
|
||||
import copy
|
||||
import re
|
||||
import time
|
||||
|
||||
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 ActionType
|
||||
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 EdgeDescription
|
||||
from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory
|
||||
from vitrage.graph.algo_driver.algorithm import Mapping
|
||||
from vitrage.graph.algo_driver.sub_graph_matching import \
|
||||
NEG_CONDITION
|
||||
@ -184,7 +187,8 @@ class ScenarioEvaluator(object):
|
||||
scenario_element,
|
||||
action.targets[TARGET])
|
||||
|
||||
actions.extend(self._get_actions_from_matches(matches,
|
||||
actions.extend(self._get_actions_from_matches(scenario.version,
|
||||
matches,
|
||||
mode,
|
||||
action))
|
||||
|
||||
@ -207,6 +211,7 @@ class ScenarioEvaluator(object):
|
||||
scenario_element)
|
||||
|
||||
def _get_actions_from_matches(self,
|
||||
scenario_version,
|
||||
combined_matches,
|
||||
mode,
|
||||
action_spec):
|
||||
@ -217,15 +222,67 @@ class ScenarioEvaluator(object):
|
||||
new_mode = ActionMode.UNDO \
|
||||
if mode == ActionMode.DO else ActionMode.DO
|
||||
|
||||
template_schema = \
|
||||
TemplateSchemaFactory().template_schema(scenario_version)
|
||||
|
||||
for match in matches:
|
||||
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)))
|
||||
self._evaluate_property_functions(template_schema, match,
|
||||
match_action_spec.properties)
|
||||
|
||||
actions.append(ActionInfo(match_action_spec, new_mode,
|
||||
match_action_spec.id, match_hash))
|
||||
|
||||
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
|
||||
def _get_action_spec(action_spec, match):
|
||||
targets = action_spec.targets
|
||||
|
@ -145,7 +145,7 @@ class ScenarioRepository(object):
|
||||
|
||||
if result.is_valid_config:
|
||||
def_validator = \
|
||||
template_schema.validator(TemplateFields.DEFINITIONS)
|
||||
template_schema.validators.get(TemplateFields.DEFINITIONS)
|
||||
result = \
|
||||
def_validator.def_template_content_validation(def_template)
|
||||
|
||||
|
@ -24,9 +24,10 @@ RELATIONSHIP = 'relationship'
|
||||
|
||||
|
||||
class Scenario(object):
|
||||
def __init__(self, id, condition, actions, subgraphs, entities,
|
||||
def __init__(self, id, version, condition, actions, subgraphs, entities,
|
||||
relationships, enabled=False):
|
||||
self.id = id
|
||||
self.version = version
|
||||
self.condition = condition
|
||||
self.actions = actions
|
||||
self.subgraphs = subgraphs
|
||||
@ -46,8 +47,9 @@ class Scenario(object):
|
||||
# noinspection PyAttributeOutsideInit
|
||||
class TemplateData(object):
|
||||
|
||||
def __init__(self, name, entities, relationships, scenarios):
|
||||
def __init__(self, name, version, entities, relationships, scenarios):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self.entities = entities
|
||||
self.relationships = relationships
|
||||
self.scenarios = scenarios
|
||||
@ -60,6 +62,14 @@ class TemplateData(object):
|
||||
def name(self, template_name):
|
||||
self._name = template_name
|
||||
|
||||
@property
|
||||
def version(self):
|
||||
return self._version
|
||||
|
||||
@version.setter
|
||||
def version(self, version):
|
||||
self._version = version
|
||||
|
||||
@property
|
||||
def entities(self):
|
||||
return self._entities
|
||||
|
15
vitrage/evaluator/template_functions/__init__.py
Normal file
15
vitrage/evaluator/template_functions/__init__.py
Normal 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'
|
15
vitrage/evaluator/template_functions/v2/__init__.py
Normal file
15
vitrage/evaluator/template_functions/v2/__init__.py
Normal 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'
|
78
vitrage/evaluator/template_functions/v2/functions.py
Normal file
78
vitrage/evaluator/template_functions/v2/functions.py
Normal 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
|
@ -57,7 +57,8 @@ class ScenarioLoader(object):
|
||||
self._extract_var_and_update_index)
|
||||
|
||||
scenarios.append(
|
||||
Scenario(scenario_id, condition, actions, subgraphs,
|
||||
Scenario(scenario_id, self._template_schema.version(),
|
||||
condition, actions, subgraphs,
|
||||
self.entities, self.relationships))
|
||||
|
||||
return scenarios
|
||||
@ -87,6 +88,7 @@ class ScenarioLoader(object):
|
||||
scenario.condition, extract_var)
|
||||
|
||||
return Scenario(id=scenario.id + '_equivalence',
|
||||
version=scenario.version,
|
||||
condition=scenario.condition,
|
||||
actions=scenario.actions,
|
||||
subgraphs=subgraphs,
|
||||
@ -99,7 +101,7 @@ class ScenarioLoader(object):
|
||||
for counter, action_def in enumerate(actions_def):
|
||||
action_id = '%s-action%s' % (scenario_id, str(counter))
|
||||
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:
|
||||
actions.append(action_loader.load(action_id, self.valid_target,
|
||||
|
@ -86,7 +86,8 @@ class TemplateLoader(object):
|
||||
self.relationships).\
|
||||
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):
|
||||
entities = {}
|
||||
|
@ -15,6 +15,8 @@ from oslo_log import log
|
||||
|
||||
from vitrage.evaluator.actions.base import ActionType
|
||||
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.execute_mistral_loader import \
|
||||
ExecuteMistralLoader
|
||||
@ -43,7 +45,7 @@ LOG = log.getLogger(__name__)
|
||||
|
||||
class TemplateSchema1(object):
|
||||
def __init__(self):
|
||||
self._validators = {
|
||||
self.validators = {
|
||||
TemplateFields.DEFINITIONS: DefinitionsValidator,
|
||||
TemplateFields.SCENARIOS: ScenarioValidator,
|
||||
ActionType.ADD_CAUSAL_RELATIONSHIP: AddCausalRelationshipValidator,
|
||||
@ -53,7 +55,7 @@ class TemplateSchema1(object):
|
||||
ActionType.SET_STATE: SetStateValidator,
|
||||
}
|
||||
|
||||
self._loaders = {
|
||||
self.loaders = {
|
||||
ActionType.ADD_CAUSAL_RELATIONSHIP: ActionLoader(),
|
||||
ActionType.EXECUTE_MISTRAL: ExecuteMistralLoader(),
|
||||
ActionType.MARK_DOWN: ActionLoader(),
|
||||
@ -61,24 +63,23 @@ class TemplateSchema1(object):
|
||||
ActionType.SET_STATE: ActionLoader(),
|
||||
}
|
||||
|
||||
def validator(self, validator_type):
|
||||
LOG.debug('Get validator. validator_type: %s. validators: %s',
|
||||
validator_type, self._validators)
|
||||
return self._validators.get(validator_type)
|
||||
self.functions = {}
|
||||
|
||||
def loader(self, loader_type):
|
||||
LOG.debug('Get loader. loader_type: %s. loaders: %s',
|
||||
loader_type, self._loaders)
|
||||
return self._loaders.get(loader_type)
|
||||
def version(self):
|
||||
return '1'
|
||||
|
||||
|
||||
class TemplateSchema2(TemplateSchema1):
|
||||
|
||||
def __init__(self):
|
||||
super(TemplateSchema2, self).__init__()
|
||||
self._validators[ActionType.EXECUTE_MISTRAL] = \
|
||||
self.validators[ActionType.EXECUTE_MISTRAL] = \
|
||||
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():
|
||||
|
@ -22,6 +22,10 @@ def get_correct_result(description):
|
||||
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):
|
||||
if msg:
|
||||
return Result(description, False, code, msg)
|
||||
|
@ -20,6 +20,7 @@ from vitrage.evaluator.template_fields import TemplateFields
|
||||
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_fault_result
|
||||
from vitrage.evaluator.template_validation.base import get_warning_result
|
||||
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)
|
||||
|
||||
|
||||
def get_content_warning_result(code):
|
||||
return get_warning_result(RESULT_DESCRIPTION, code)
|
||||
|
||||
|
||||
def validate_template_id(definitions_index, id_to_check):
|
||||
if id_to_check not in definitions_index:
|
||||
msg = status_msgs[3] + ' template id: %s' % id_to_check
|
||||
|
@ -30,7 +30,8 @@ def content_validation(template, def_templates=None):
|
||||
template_definitions = {}
|
||||
|
||||
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 not def_validator:
|
||||
@ -71,7 +72,7 @@ def content_validation(template, def_templates=None):
|
||||
relationship_index)
|
||||
|
||||
if result.is_valid_config:
|
||||
scenario_validator = template_schema.validator(
|
||||
scenario_validator = template_schema.validators.get(
|
||||
TemplateFields.SCENARIOS)
|
||||
scenarios = template[TemplateFields.SCENARIOS]
|
||||
definitions_index = entities_index.copy()
|
||||
|
@ -15,6 +15,7 @@
|
||||
from oslo_log import log
|
||||
|
||||
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_validation.content.base import \
|
||||
ActionValidator
|
||||
@ -38,4 +39,9 @@ class ExecuteMistralValidator(ActionValidator):
|
||||
LOG.error('%s status code: %s' % (status_msgs[133], 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()
|
||||
|
@ -184,7 +184,7 @@ class ScenarioValidator(object):
|
||||
@staticmethod
|
||||
def _validate_scenario_action(template_schema, def_index, action):
|
||||
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:
|
||||
LOG.error('%s status code: %s' % (status_msgs[120], 120))
|
||||
|
@ -13,9 +13,11 @@
|
||||
# under the License.
|
||||
|
||||
from oslo_log import log
|
||||
import re
|
||||
|
||||
from vitrage.evaluator.actions.recipes.execute_mistral import INPUT
|
||||
from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW
|
||||
from vitrage.evaluator.base import is_function
|
||||
from vitrage.evaluator.template_fields import TemplateFields
|
||||
from vitrage.evaluator.template_validation.content.base import \
|
||||
ActionValidator
|
||||
@ -23,6 +25,8 @@ from vitrage.evaluator.template_validation.content.base import \
|
||||
get_content_correct_result
|
||||
from vitrage.evaluator.template_validation.content.base import \
|
||||
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
|
||||
|
||||
|
||||
@ -44,4 +48,11 @@ class ExecuteMistralValidator(ActionValidator):
|
||||
LOG.error('%s status code: %s' % (status_msgs[136], 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()
|
||||
|
@ -84,6 +84,9 @@ status_msgs = {
|
||||
135: 'condition must contain a common entity for all \'or\' clauses',
|
||||
136: 'Input parameters for the Mistral workflow in execute_mistral action '
|
||||
'must be placed under an \'input\' block ',
|
||||
137: 'Functions are supported only from version 2',
|
||||
138: 'Warning: only open or close parenthesis exists. Did you try to use '
|
||||
'a function?',
|
||||
|
||||
# def_templates status messages 140-159
|
||||
140: 'At least one template must be included',
|
||||
|
@ -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)
|
15
vitrage/tests/unit/evaluator/template_functions/__init__.py
Normal file
15
vitrage/tests/unit/evaluator/template_functions/__init__.py
Normal 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'
|
@ -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}
|
@ -47,6 +47,12 @@ class ValidatorTest(base.BaseTest):
|
||||
self.assertTrue(result.comment.startswith(status_msgs[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
|
||||
def _hide_useless_logging_messages():
|
||||
|
||||
|
@ -98,13 +98,16 @@ class BaseExecuteMistralValidatorTest(ActionValidatorTest):
|
||||
return action
|
||||
|
||||
@staticmethod
|
||||
def _create_v1_execute_mistral_action(workflow, host, host_state):
|
||||
def _create_v1_execute_mistral_action(workflow, host, host_state,
|
||||
**kwargs):
|
||||
|
||||
properties = {
|
||||
WORKFLOW: workflow,
|
||||
'host': host,
|
||||
'host_state': host_state
|
||||
}
|
||||
properties.update(kwargs)
|
||||
|
||||
action = {
|
||||
TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL,
|
||||
TemplateFields.PROPERTIES: properties
|
||||
@ -113,16 +116,19 @@ class BaseExecuteMistralValidatorTest(ActionValidatorTest):
|
||||
return action
|
||||
|
||||
@staticmethod
|
||||
def _create_v2_execute_mistral_action(workflow, host, host_state):
|
||||
def _create_v2_execute_mistral_action(workflow, host, host_state,
|
||||
**kwargs):
|
||||
|
||||
input_props = {
|
||||
'host': host,
|
||||
'host_state': host_state
|
||||
}
|
||||
input_props.update(kwargs)
|
||||
properties = {
|
||||
WORKFLOW: workflow,
|
||||
'input': input_props
|
||||
}
|
||||
|
||||
action = {
|
||||
TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL,
|
||||
TemplateFields.PROPERTIES: properties
|
||||
|
@ -289,7 +289,7 @@ class TemplateContentValidatorTest(ValidatorTest):
|
||||
def test_validate_template_with_version_1(self):
|
||||
invalid_version_path = \
|
||||
VERSION_TEMPLATE_DIR % (utils.get_resources_dir(),
|
||||
"version1.yaml")
|
||||
"v1/version1.yaml")
|
||||
template = file_utils.load_yaml_file(invalid_version_path)
|
||||
self._execute_and_assert_with_correct_result(template)
|
||||
|
||||
|
@ -60,6 +60,34 @@ class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest):
|
||||
# Test assertions
|
||||
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):
|
||||
return self.\
|
||||
_create_v1_execute_mistral_action(workflow, host, host_state)
|
||||
|
@ -63,6 +63,53 @@ class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest):
|
||||
self._validate_execute_mistral_action_without_additional_props(
|
||||
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):
|
||||
return self.\
|
||||
_create_v2_execute_mistral_action(workflow, host, host_state)
|
||||
|
@ -228,7 +228,7 @@ class TemplateSyntaxValidatorTest(base.BaseTest):
|
||||
self._test_execution_with_correct_result(template)
|
||||
|
||||
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)
|
||||
self._test_execution_with_correct_result(template)
|
||||
|
||||
|
@ -38,8 +38,8 @@ class BasicTemplateTest(base.BaseTest):
|
||||
|
||||
BASIC_TEMPLATE = 'basic.yaml'
|
||||
BASIC_TEMPLATE_WITH_INCLUDE = 'basic_with_include.yaml'
|
||||
V1_MISTRAL_TEMPLATE = 'v1_execute_mistral.yaml'
|
||||
V2_MISTRAL_TEMPLATE = 'v2_execute_mistral.yaml'
|
||||
V1_MISTRAL_TEMPLATE = 'v1/v1_execute_mistral.yaml'
|
||||
V2_MISTRAL_TEMPLATE = 'v2/v2_execute_mistral.yaml'
|
||||
DEF_TEMPLATE_TESTS_DIR = utils.get_resources_dir() +\
|
||||
'/templates/def_template_tests'
|
||||
|
||||
@ -135,6 +135,7 @@ class BasicTemplateTest(base.BaseTest):
|
||||
|
||||
expected_scenario = Scenario(
|
||||
id='basic_template_with_include-scenario0',
|
||||
version=1,
|
||||
condition=[
|
||||
[ConditionVar(symbol_name='alarm_on_host',
|
||||
positive=True)]],
|
||||
@ -209,6 +210,7 @@ class BasicTemplateTest(base.BaseTest):
|
||||
|
||||
expected_scenario = Scenario(
|
||||
id='basic_template-scenario0',
|
||||
version=1,
|
||||
condition=[
|
||||
[ConditionVar(symbol_name='alarm_on_host',
|
||||
positive=True)]],
|
||||
|
@ -12,6 +12,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
from oslo_log import log as logging
|
||||
from testtools.matchers import HasLength
|
||||
|
||||
@ -45,6 +46,7 @@ class TestMistralNotifier(BaseTestEvents):
|
||||
|
||||
TRIGGER_ALARM_1 = "notifiers.mistral.trigger.alarm.1"
|
||||
TRIGGER_ALARM_2 = "notifiers.mistral.trigger.alarm.2"
|
||||
TRIGGER_ALARM_FOR_FUNCTION = "notifiers.mistral.trigger.alarm.for.function"
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
@ -59,6 +61,36 @@ class TestMistralNotifier(BaseTestEvents):
|
||||
def test_execute_mistral_v2(self):
|
||||
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):
|
||||
workflows = self.mistral_client.workflows.list()
|
||||
self.assertIsNotNone(workflows, 'Failed to get the list of workflows')
|
||||
|
@ -7,20 +7,29 @@ definitions:
|
||||
- entity:
|
||||
category: ALARM
|
||||
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:
|
||||
category: RESOURCE
|
||||
type: nova.host
|
||||
template_id: host
|
||||
relationships:
|
||||
- relationship:
|
||||
source: alarm
|
||||
source: alarm_2
|
||||
relationship_type: on
|
||||
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:
|
||||
- scenario:
|
||||
condition: alarm_on_host
|
||||
condition: alarm_2_on_host
|
||||
actions:
|
||||
- action:
|
||||
action_type: execute_mistral
|
||||
@ -28,3 +37,12 @@ scenarios:
|
||||
workflow: wf_for_tempest_test_1234
|
||||
input:
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user