Merge "Introduce the execute-mistral action. This patch includes validating the action, and converting it to a general-purpose ExecuteExternal step."

This commit is contained in:
Jenkins 2017-07-05 09:46:33 +00:00 committed by Gerrit Code Review
commit 32de6d3b8c
12 changed files with 312 additions and 16 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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