diff --git a/doc/source/template_validation_status_code.rst b/doc/source/template_validation_status_code.rst index 0d405430f..e76ae89c8 100644 --- a/doc/source/template_validation_status_code.rst +++ b/doc/source/template_validation_status_code.rst @@ -101,3 +101,6 @@ The following describes all the possible status code and their messages: | 132 | add_causal_relationship action requires action_target to| content | | | be ALARM | | +------------------+---------------------------------------------------------+-------------------------------+ +| 133 | execute_mistral action must contain workflow field in | content | +| | properties block | | ++------------------+---------------------------------------------------------+-------------------------------+ diff --git a/vitrage/evaluator/actions/action_executor.py b/vitrage/evaluator/actions/action_executor.py index 805bab475..685f0c0cd 100644 --- a/vitrage/evaluator/actions/action_executor.py +++ b/vitrage/evaluator/actions/action_executor.py @@ -25,12 +25,14 @@ from vitrage.evaluator.actions.evaluator_event_transformer \ import VITRAGE_DATASOURCE from vitrage.evaluator.actions.recipes.action_steps import ADD_EDGE from vitrage.evaluator.actions.recipes.action_steps import ADD_VERTEX +from vitrage.evaluator.actions.recipes.action_steps import EXECUTE_EXTERNAL from vitrage.evaluator.actions.recipes.action_steps import REMOVE_EDGE from vitrage.evaluator.actions.recipes.action_steps import REMOVE_VERTEX from vitrage.evaluator.actions.recipes.action_steps import UPDATE_VERTEX from vitrage.evaluator.actions.recipes.add_causal_relationship import \ AddCausalRelationship from vitrage.evaluator.actions.recipes.base import EVALUATOR_EVENT_TYPE +from vitrage.evaluator.actions.recipes.execute_mistral import ExecuteMistral from vitrage.evaluator.actions.recipes.mark_down import MarkDown from vitrage.evaluator.actions.recipes.raise_alarm import RaiseAlarm from vitrage.evaluator.actions.recipes.set_state import SetState @@ -44,11 +46,12 @@ class ActionExecutor(object): self.action_recipes = ActionExecutor._register_action_recipes() self.action_step_defs = { - ADD_VERTEX: self.add_vertex, - REMOVE_VERTEX: self.remove_vertex, - UPDATE_VERTEX: self.update_vertex, - ADD_EDGE: self.add_edge, - REMOVE_EDGE: self.remove_edge, + ADD_VERTEX: self._add_vertex, + REMOVE_VERTEX: self._remove_vertex, + UPDATE_VERTEX: self._update_vertex, + ADD_EDGE: self._add_edge, + REMOVE_EDGE: self._remove_edge, + EXECUTE_EXTERNAL: self._execute_external, } def execute(self, action_spec, action_mode): @@ -62,7 +65,7 @@ class ActionExecutor(object): for step in steps: self.action_step_defs[step.type](step.params) - def add_vertex(self, params): + def _add_vertex(self, params): event = copy.deepcopy(params) ActionExecutor._add_default_properties(event) @@ -70,7 +73,7 @@ class ActionExecutor(object): self.event_queue.put(event) - def update_vertex(self, params): + def _update_vertex(self, params): event = copy.deepcopy(params) ActionExecutor._add_default_properties(event) @@ -78,14 +81,14 @@ class ActionExecutor(object): self.event_queue.put(event) - def remove_vertex(self, params): + def _remove_vertex(self, params): event = copy.deepcopy(params) ActionExecutor._add_default_properties(event) event[EVALUATOR_EVENT_TYPE] = REMOVE_VERTEX self.event_queue.put(event) - def add_edge(self, params): + def _add_edge(self, params): event = copy.deepcopy(params) ActionExecutor._add_default_properties(event) @@ -93,7 +96,7 @@ class ActionExecutor(object): self.event_queue.put(event) - def remove_edge(self, params): + def _remove_edge(self, params): event = copy.deepcopy(params) ActionExecutor._add_default_properties(event) @@ -101,6 +104,12 @@ class ActionExecutor(object): self.event_queue.put(event) + def _execute_external(self, params): + + # TODO(ifat_afek): send to a dedicated queue + # external_engine = params[EXECUTION_ENGINE] + pass + @staticmethod def _add_default_properties(event): @@ -129,4 +138,7 @@ class ActionExecutor(object): recipes[ActionType.MARK_DOWN] = importutils.import_object( "%s.%s" % (MarkDown.__module__, MarkDown.__name__)) + recipes[ActionType.EXECUTE_MISTRAL] = importutils.import_object( + "%s.%s" % (ExecuteMistral.__module__, ExecuteMistral.__name__)) + return recipes diff --git a/vitrage/evaluator/actions/base.py b/vitrage/evaluator/actions/base.py index 6f93d9b73..e010f42a4 100644 --- a/vitrage/evaluator/actions/base.py +++ b/vitrage/evaluator/actions/base.py @@ -19,11 +19,13 @@ class ActionType(object): RAISE_ALARM = 'raise_alarm' ADD_CAUSAL_RELATIONSHIP = 'add_causal_relationship' MARK_DOWN = 'mark_down' + EXECUTE_MISTRAL = 'execute_mistral' action_types = [ActionType.SET_STATE, ActionType.RAISE_ALARM, ActionType.ADD_CAUSAL_RELATIONSHIP, - ActionType.MARK_DOWN] + ActionType.MARK_DOWN, + ActionType.EXECUTE_MISTRAL] class ActionMode(object): diff --git a/vitrage/evaluator/actions/recipes/action_steps.py b/vitrage/evaluator/actions/recipes/action_steps.py index d69075267..4bdfdc181 100644 --- a/vitrage/evaluator/actions/recipes/action_steps.py +++ b/vitrage/evaluator/actions/recipes/action_steps.py @@ -17,3 +17,6 @@ REMOVE_VERTEX = 'remove_vertex' UPDATE_VERTEX = 'update_vertex' ADD_EDGE = 'add_edge' REMOVE_EDGE = 'remove_edge' +EXECUTE_EXTERNAL = 'execute_external' + +EXECUTION_ENGINE = 'execution_engine' diff --git a/vitrage/evaluator/actions/recipes/execute_mistral.py b/vitrage/evaluator/actions/recipes/execute_mistral.py new file mode 100644 index 000000000..315d2c023 --- /dev/null +++ b/vitrage/evaluator/actions/recipes/execute_mistral.py @@ -0,0 +1,59 @@ +# Copyright 2017 - 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.actions.recipes.action_steps import EXECUTE_EXTERNAL +from vitrage.evaluator.actions.recipes.action_steps import EXECUTION_ENGINE +from vitrage.evaluator.actions.recipes import base +from vitrage.evaluator.actions.recipes.base import ActionStepWrapper + + +MISTRAL = 'mistral' +WORKFLOW = 'workflow' + + +class ExecuteMistral(base.Recipe): + """Execute a Mistral workflow + + The 'get_do_recipe' and 'get_undo_recipe' receive action_spec as input. + The action_spec contains the following fields: type and properties. + + example input: + + action_spec = ActionSpecs('type'= {'execute_mistral'}, + 'properties' = {workflow : wf_1, + host: host_2, + host_status: down} + """ + + @staticmethod + def get_do_recipe(action_spec): + + execute_external_step = ExecuteMistral._get_execute_external_step( + action_spec.properties + ) + + return [execute_external_step] + + @staticmethod + def get_undo_recipe(action_spec): + # No undo for execute an external action + return [] + + @staticmethod + def _get_execute_external_step(properties): + + properties[EXECUTION_ENGINE] = MISTRAL + execute_external_step = ActionStepWrapper(EXECUTE_EXTERNAL, + properties) + + return execute_external_step diff --git a/vitrage/evaluator/scenario_evaluator.py b/vitrage/evaluator/scenario_evaluator.py index 62f495ef6..d0da932c4 100644 --- a/vitrage/evaluator/scenario_evaluator.py +++ b/vitrage/evaluator/scenario_evaluator.py @@ -382,7 +382,8 @@ class ActionTracker(object): ActionType.SET_STATE: pt.SetStateTools(all_scores), ActionType.RAISE_ALARM: pt.RaiseAlarmTools(alarms_score), ActionType.ADD_CAUSAL_RELATIONSHIP: pt.BaselineTools, - ActionType.MARK_DOWN: pt.BaselineTools + ActionType.MARK_DOWN: pt.BaselineTools, + ActionType.EXECUTE_MISTRAL: pt.BaselineTools } def get_key(self, action_specs): diff --git a/vitrage/evaluator/template_data.py b/vitrage/evaluator/template_data.py index 13bd2b784..cf7740c4b 100644 --- a/vitrage/evaluator/template_data.py +++ b/vitrage/evaluator/template_data.py @@ -274,7 +274,7 @@ class TemplateData(object): action_dict = action_def[TFields.ACTION] action_type = action_dict[TFields.ACTION_TYPE] - targets = action_dict[TFields.ACTION_TARGET] + targets = action_dict.get(TFields.ACTION_TARGET, {}) properties = action_dict.get(TFields.PROPERTIES, {}) actions.append(ActionSpecs(action_type, targets, properties)) diff --git a/vitrage/evaluator/template_validation/content/execute_mistral_validator.py b/vitrage/evaluator/template_validation/content/execute_mistral_validator.py new file mode 100644 index 000000000..1a18c3353 --- /dev/null +++ b/vitrage/evaluator/template_validation/content/execute_mistral_validator.py @@ -0,0 +1,41 @@ +# Copyright 2017 - 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 + +from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_validation.content.base import \ + ActionValidator +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.status_messages import status_msgs + + +LOG = log.getLogger(__name__) + + +class ExecuteMistralValidator(ActionValidator): + + @staticmethod + def validate(action, definitions_index): + properties = action[TemplateFields.PROPERTIES] + + if WORKFLOW not in properties or not properties[WORKFLOW]: + LOG.error('%s status code: %s' % (status_msgs[133], 133)) + return get_content_fault_result(133) + + return get_content_correct_result() diff --git a/vitrage/evaluator/template_validation/status_messages.py b/vitrage/evaluator/template_validation/status_messages.py index 58efa8ed7..dfddab409 100644 --- a/vitrage/evaluator/template_validation/status_messages.py +++ b/vitrage/evaluator/template_validation/status_messages.py @@ -62,8 +62,8 @@ status_msgs = { '{actions}'.format(actions=action_types), 121: 'At least one action must be defined.', 122: 'Action field is required.', - 123: 'Relationship definition must contain action_type field.', - 124: 'Relationship definition must contain action_target field.', + 123: 'Action definition must contain action_type field.', + 124: 'Action definition must contain action_target field.', 125: 'raise_alarm action must contain alarm_name field in properties ' 'block.', 126: 'raise_alarm action must contain severity field in properties block.', @@ -74,6 +74,8 @@ status_msgs = { 'in target_action block.', 131: 'mark_down action must contain \'target\' field in' ' \'target_action\' block.', - 132: 'add_causal_relationship action requires action_target to be ALARM' + 132: 'add_causal_relationship action requires action_target to be ALARM', + 133: 'execute_mistral action must contain workflow field in properties ' + 'block' } diff --git a/vitrage/tests/base.py b/vitrage/tests/base.py index b4ef99b1c..5864d259f 100644 --- a/vitrage/tests/base.py +++ b/vitrage/tests/base.py @@ -17,6 +17,10 @@ from oslo_utils import timeutils # noinspection PyPackageRequirements from oslotest import base import sys +from testtools.matchers import HasLength + + +IsEmpty = lambda: HasLength(0) class BaseTest(base.BaseTestCase): diff --git a/vitrage/tests/unit/evaluator/recipes/test_execute_mistral.py b/vitrage/tests/unit/evaluator/recipes/test_execute_mistral.py new file mode 100644 index 000000000..779f2532e --- /dev/null +++ b/vitrage/tests/unit/evaluator/recipes/test_execute_mistral.py @@ -0,0 +1,70 @@ +# Copyright 2017 - 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 testtools.matchers import HasLength +from vitrage.evaluator.actions.base import ActionType +from vitrage.evaluator.actions.recipes.action_steps import EXECUTE_EXTERNAL +from vitrage.evaluator.actions.recipes.action_steps import EXECUTION_ENGINE +from vitrage.evaluator.actions.recipes.execute_mistral import ExecuteMistral +from vitrage.evaluator.actions.recipes.execute_mistral import MISTRAL +from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW +from vitrage.evaluator.template_data import ActionSpecs +from vitrage.tests.base import BaseTest +from vitrage.tests.base import IsEmpty + + +class RaiseAlarmRecipeTest(BaseTest): + + # noinspection PyPep8Naming + @classmethod + def setUpClass(cls): + + cls.props = {EXECUTION_ENGINE: MISTRAL, + WORKFLOW: 'wf_4', + 'host': 'host5', + 'state': 'ok'} + cls.action_spec = ActionSpecs(ActionType.EXECUTE_MISTRAL, + {}, + cls.props) + + def test_get_do_recipe(self): + + # Test Action + action_steps = ExecuteMistral.get_do_recipe(self.action_spec) + + # Test Assertions + + # expecting for one step: [execute_external] + self.assertThat(action_steps, HasLength(1)) + + self.assertEqual(EXECUTE_EXTERNAL, action_steps[0].type) + execute_external_step_params = action_steps[0].params + self.assertIsNotNone(execute_external_step_params) + self.assertLessEqual(2, len(execute_external_step_params)) + + execution_engine = execute_external_step_params[EXECUTION_ENGINE] + self.assertEqual(self.props[EXECUTION_ENGINE], execution_engine) + + workflow = execute_external_step_params[WORKFLOW] + self.assertEqual(self.props[WORKFLOW], workflow) + + def test_get_undo_recipe(self): + + # Test Action + action_steps = ExecuteMistral.get_undo_recipe(self.action_spec) + + # Test Assertions + + # expecting for zero steps (no undo for this action) + self.assertThat(action_steps, IsEmpty()) diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py new file mode 100644 index 000000000..640acdaa2 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py @@ -0,0 +1,99 @@ +# Copyright 2017 - 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.actions.base import ActionType +from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_validation.content.execute_mistral_validator \ + import ExecuteMistralValidator +from vitrage.tests.unit.evaluator.template_validation.content.base import \ + ActionValidatorTest +from vitrage.tests.unit.evaluator.template_validation.content.base import \ + DEFINITIONS_INDEX_MOCK + + +class ExecuteMistralValidatorTest(ActionValidatorTest): + + def test_validate_execute_mistral_action(self): + + self._validate_action( + self._create_execute_mistral_action('wf_1', 'host_2', 'down'), + ExecuteMistralValidator.validate + ) + + def test_validate_execute_mistral_action_without_workflow(self): + + # Test setup + idx = DEFINITIONS_INDEX_MOCK.copy() + action = self._create_execute_mistral_action('wf_1', 'host_2', 'down') + action[TemplateFields.PROPERTIES].pop(WORKFLOW) + + # Test action + result = ExecuteMistralValidator.validate(action, idx) + + # Test assertions + self._assert_fault_result(result, 133) + + def test_validate_execute_mistral_action_with_empty_workflow(self): + + # Test setup + idx = DEFINITIONS_INDEX_MOCK.copy() + action = self._create_execute_mistral_action('', 'host_2', 'down') + + # Test action + result = ExecuteMistralValidator.validate(action, idx) + + # Test assertions + self._assert_fault_result(result, 133) + + def test_validate_execute_mistral_action_with_none_workflow(self): + + # Test setup + idx = DEFINITIONS_INDEX_MOCK.copy() + action = self._create_execute_mistral_action(None, 'host_2', 'down') + + # Test action + result = ExecuteMistralValidator.validate(action, idx) + + # Test assertions + self._assert_fault_result(result, 133) + + def test_validate_execute_mistral_action_without_additional_params(self): + + # Test setup - having only the 'workflow' param is a legal config + idx = DEFINITIONS_INDEX_MOCK.copy() + action = self._create_execute_mistral_action('wf_1', 'host_2', 'down') + action[TemplateFields.PROPERTIES].pop('host') + action[TemplateFields.PROPERTIES].pop('host_state') + + # Test action + result = ExecuteMistralValidator.validate(action, idx) + + # Test assertions + self._assert_correct_result(result) + + @staticmethod + def _create_execute_mistral_action(workflow, host, host_state): + + properties = { + WORKFLOW: workflow, + 'host': host, + 'host_state': host_state + } + action = { + TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL, + TemplateFields.PROPERTIES: properties + } + + return action