diff --git a/doc/source/contributor/templates-loading.rst b/doc/source/contributor/templates-loading.rst index f4b09192b..622020ac8 100644 --- a/doc/source/contributor/templates-loading.rst +++ b/doc/source/contributor/templates-loading.rst @@ -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 -- diff --git a/doc/source/contributor/vitrage-template-format.rst b/doc/source/contributor/vitrage-template-format.rst index c02563504..530477890 100644 --- a/doc/source/contributor/vitrage-template-format.rst +++ b/doc/source/contributor/vitrage-template-format.rst @@ -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 ----------------- diff --git a/releasenotes/notes/support-template-functions-dcb2d2e1e63e9a5d.yaml b/releasenotes/notes/support-template-functions-dcb2d2e1e63e9a5d.yaml new file mode 100644 index 000000000..f5efb7774 --- /dev/null +++ b/releasenotes/notes/support-template-functions-dcb2d2e1e63e9a5d.yaml @@ -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. diff --git a/vitrage/evaluator/base.py b/vitrage/evaluator/base.py index 1ebca27f4..9b93074db 100644 --- a/vitrage/evaluator/base.py +++ b/vitrage/evaluator/base.py @@ -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) diff --git a/vitrage/evaluator/scenario_evaluator.py b/vitrage/evaluator/scenario_evaluator.py index 408f6b367..1ffaafb47 100644 --- a/vitrage/evaluator/scenario_evaluator.py +++ b/vitrage/evaluator/scenario_evaluator.py @@ -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 diff --git a/vitrage/evaluator/scenario_repository.py b/vitrage/evaluator/scenario_repository.py index 9917f4ed7..8c5e457ce 100644 --- a/vitrage/evaluator/scenario_repository.py +++ b/vitrage/evaluator/scenario_repository.py @@ -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) diff --git a/vitrage/evaluator/template_data.py b/vitrage/evaluator/template_data.py index 50981515e..6472c2205 100644 --- a/vitrage/evaluator/template_data.py +++ b/vitrage/evaluator/template_data.py @@ -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 diff --git a/vitrage/evaluator/template_functions/__init__.py b/vitrage/evaluator/template_functions/__init__.py new file mode 100644 index 000000000..5c2b8b158 --- /dev/null +++ b/vitrage/evaluator/template_functions/__init__.py @@ -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' diff --git a/vitrage/evaluator/template_functions/v2/__init__.py b/vitrage/evaluator/template_functions/v2/__init__.py new file mode 100644 index 000000000..5c2b8b158 --- /dev/null +++ b/vitrage/evaluator/template_functions/v2/__init__.py @@ -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' diff --git a/vitrage/evaluator/template_functions/v2/functions.py b/vitrage/evaluator/template_functions/v2/functions.py new file mode 100644 index 000000000..9d20b6176 --- /dev/null +++ b/vitrage/evaluator/template_functions/v2/functions.py @@ -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 diff --git a/vitrage/evaluator/template_loading/scenario_loader.py b/vitrage/evaluator/template_loading/scenario_loader.py index d76404152..b0225b366 100644 --- a/vitrage/evaluator/template_loading/scenario_loader.py +++ b/vitrage/evaluator/template_loading/scenario_loader.py @@ -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, diff --git a/vitrage/evaluator/template_loading/template_loader.py b/vitrage/evaluator/template_loading/template_loader.py index 0336fcec3..ec9afacf1 100644 --- a/vitrage/evaluator/template_loading/template_loader.py +++ b/vitrage/evaluator/template_loading/template_loader.py @@ -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 = {} diff --git a/vitrage/evaluator/template_schemas.py b/vitrage/evaluator/template_schemas.py index 986ea6890..462843982 100644 --- a/vitrage/evaluator/template_schemas.py +++ b/vitrage/evaluator/template_schemas.py @@ -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(): diff --git a/vitrage/evaluator/template_validation/base.py b/vitrage/evaluator/template_validation/base.py index 4797c05b9..4f46b06a1 100644 --- a/vitrage/evaluator/template_validation/base.py +++ b/vitrage/evaluator/template_validation/base.py @@ -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) diff --git a/vitrage/evaluator/template_validation/content/base.py b/vitrage/evaluator/template_validation/content/base.py index 6db2cca94..74665934c 100644 --- a/vitrage/evaluator/template_validation/content/base.py +++ b/vitrage/evaluator/template_validation/content/base.py @@ -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 diff --git a/vitrage/evaluator/template_validation/content/template_content_validator.py b/vitrage/evaluator/template_validation/content/template_content_validator.py index e8562b88c..4bdb16c89 100644 --- a/vitrage/evaluator/template_validation/content/template_content_validator.py +++ b/vitrage/evaluator/template_validation/content/template_content_validator.py @@ -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() diff --git a/vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py b/vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py index 1a18c3353..1c532926d 100644 --- a/vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py +++ b/vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py @@ -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() diff --git a/vitrage/evaluator/template_validation/content/v1/scenario_validator.py b/vitrage/evaluator/template_validation/content/v1/scenario_validator.py index 254246f2b..2d1ecd99f 100644 --- a/vitrage/evaluator/template_validation/content/v1/scenario_validator.py +++ b/vitrage/evaluator/template_validation/content/v1/scenario_validator.py @@ -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)) diff --git a/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py b/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py index 3f0f5b60e..b08a36594 100644 --- a/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py +++ b/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py @@ -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() diff --git a/vitrage/evaluator/template_validation/status_messages.py b/vitrage/evaluator/template_validation/status_messages.py index 51c3cf18b..6f98fa0f9 100644 --- a/vitrage/evaluator/template_validation/status_messages.py +++ b/vitrage/evaluator/template_validation/status_messages.py @@ -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', diff --git a/vitrage/tests/resources/templates/version/v1_execute_mistral.yaml b/vitrage/tests/resources/templates/version/v1/v1_execute_mistral.yaml similarity index 100% rename from vitrage/tests/resources/templates/version/v1_execute_mistral.yaml rename to vitrage/tests/resources/templates/version/v1/v1_execute_mistral.yaml diff --git a/vitrage/tests/resources/templates/version/version1.yaml b/vitrage/tests/resources/templates/version/v1/version1.yaml similarity index 100% rename from vitrage/tests/resources/templates/version/version1.yaml rename to vitrage/tests/resources/templates/version/v1/version1.yaml diff --git a/vitrage/tests/resources/templates/version/v2_execute_mistral.yaml b/vitrage/tests/resources/templates/version/v2/v2_execute_mistral.yaml similarity index 100% rename from vitrage/tests/resources/templates/version/v2_execute_mistral.yaml rename to vitrage/tests/resources/templates/version/v2/v2_execute_mistral.yaml diff --git a/vitrage/tests/resources/templates/version/v2/v2_with_func.yaml b/vitrage/tests/resources/templates/version/v2/v2_with_func.yaml new file mode 100644 index 000000000..8b55c1b48 --- /dev/null +++ b/vitrage/tests/resources/templates/version/v2/v2_with_func.yaml @@ -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) diff --git a/vitrage/tests/unit/evaluator/template_functions/__init__.py b/vitrage/tests/unit/evaluator/template_functions/__init__.py new file mode 100644 index 000000000..5c2b8b158 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_functions/__init__.py @@ -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' diff --git a/vitrage/tests/unit/evaluator/template_functions/test_template_functions.py b/vitrage/tests/unit/evaluator/template_functions/test_template_functions.py new file mode 100644 index 000000000..29a42de0c --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_functions/test_template_functions.py @@ -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} diff --git a/vitrage/tests/unit/evaluator/template_validation/content/base.py b/vitrage/tests/unit/evaluator/template_validation/content/base.py index c8e46a5e7..6fb63fb98 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/base.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/base.py @@ -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(): diff --git a/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py index 6ca92c4d5..def312b80 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py @@ -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 diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_template_content_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/test_template_content_validator.py index 701fa4bf3..23b1dabbb 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_template_content_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/test_template_content_validator.py @@ -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) diff --git a/vitrage/tests/unit/evaluator/template_validation/content/v1/test_execute_mistral_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_execute_mistral_validator.py index 2ea11aa5d..a71cf5a11 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/v1/test_execute_mistral_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_execute_mistral_validator.py @@ -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) diff --git a/vitrage/tests/unit/evaluator/template_validation/content/v2/test_execute_mistral_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v2/test_execute_mistral_validator.py index fb21cc608..d8733dd2b 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/v2/test_execute_mistral_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v2/test_execute_mistral_validator.py @@ -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) diff --git a/vitrage/tests/unit/evaluator/template_validation/test_template_syntax_validator.py b/vitrage/tests/unit/evaluator/template_validation/test_template_syntax_validator.py index 3d4a27dd5..299cfa277 100644 --- a/vitrage/tests/unit/evaluator/template_validation/test_template_syntax_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/test_template_syntax_validator.py @@ -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) diff --git a/vitrage/tests/unit/evaluator/test_template_loader.py b/vitrage/tests/unit/evaluator/test_template_loader.py index 3f37c2843..5fabec383 100644 --- a/vitrage/tests/unit/evaluator/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/test_template_loader.py @@ -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)]], diff --git a/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py b/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py index 16729117f..bb1452d18 100644 --- a/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py +++ b/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py @@ -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') diff --git a/vitrage_tempest_tests/tests/resources/templates/api/v2_execute_mistral.yaml b/vitrage_tempest_tests/tests/resources/templates/api/v2_execute_mistral.yaml index 60585c334..889e75873 100644 --- a/vitrage_tempest_tests/tests/resources/templates/api/v2_execute_mistral.yaml +++ b/vitrage_tempest_tests/tests/resources/templates/api/v2_execute_mistral.yaml @@ -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)