diff --git a/doc/source/contributor/template_validation_status_code.rst b/doc/source/contributor/template_validation_status_code.rst index 3cf25ca79..05fef20ff 100644 --- a/doc/source/contributor/template_validation_status_code.rst +++ b/doc/source/contributor/template_validation_status_code.rst @@ -117,6 +117,9 @@ The following describes all the possible status code and their messages: | 135 | condition must contain a common entity for all 'or' | content | | | clauses | | +------------------+---------------------------------------------------------+-------------------------------+ +| 136 | Input parameters for Mistral workflow in execute_mistral| content (version 2) | +| | action must be placed under an 'input' block | | ++------------------+---------------------------------------------------------+-------------------------------+ | 140 | At least one template must be included | syntax | +------------------+---------------------------------------------------------+-------------------------------+ | 141 | Name field is unspecified for include | syntax | diff --git a/doc/source/contributor/vitrage-template-format.rst b/doc/source/contributor/vitrage-template-format.rst index ac133b9b3..c02563504 100644 --- a/doc/source/contributor/vitrage-template-format.rst +++ b/doc/source/contributor/vitrage-template-format.rst @@ -11,6 +11,12 @@ This page describes the format of the Vitrage templates, with some examples and open questions on extending this format. Additionally, a short guide on adding templates is presented. +*Note:* This document refers to Vitrage templates version 2. The documentation +of version 1 can be found here_ + +.. _here: https://docs.openstack.org/vitrage/pike/ + + Template Structure ================== The template is written in YAML language, with the following structure: @@ -463,9 +469,10 @@ its parameters. action: action_type: execute_mistral properties: - workflow: demo_workflow # mandatory. The name of the workflow to be executed - farewell: Goodbye and Good Luck! # optional. A list of properties to be passed to the workflow - employee: John Smith # optional. A list of properties to be passed to the workflow + workflow: demo_workflow # mandatory. The name of the workflow to be executed + input: # optional. A list of properties to be passed to the workflow + farewell: Goodbye and Good Luck! + employee: John Smith Future support & Open Issues diff --git a/releasenotes/notes/refactor-execute-mistral-action-fc0fac84c07e1784.yaml b/releasenotes/notes/refactor-execute-mistral-action-fc0fac84c07e1784.yaml new file mode 100644 index 000000000..dcafbed73 --- /dev/null +++ b/releasenotes/notes/refactor-execute-mistral-action-fc0fac84c07e1784.yaml @@ -0,0 +1,6 @@ +--- +features: + - Refactored the ``execute-mistral`` action. All input parameters should + appear under an ``input`` section. The change takes effect in template + version 2. ``execute-mistral`` actions from version 1 are automatically + converted to the new format. diff --git a/vitrage/evaluator/__init__.py b/vitrage/evaluator/__init__.py index 66a1c008c..6bd23ed27 100644 --- a/vitrage/evaluator/__init__.py +++ b/vitrage/evaluator/__init__.py @@ -14,6 +14,8 @@ from oslo_config import cfg +from vitrage.evaluator.template_schemas import init_template_schemas + # Register options for the service OPTS = [ @@ -38,3 +40,5 @@ OPTS = [ 'determined, else a default worker count of 1 is returned.' ), ] + +init_template_schemas() diff --git a/vitrage/evaluator/actions/recipes/execute_mistral.py b/vitrage/evaluator/actions/recipes/execute_mistral.py index 315d2c023..0f2db2c66 100644 --- a/vitrage/evaluator/actions/recipes/execute_mistral.py +++ b/vitrage/evaluator/actions/recipes/execute_mistral.py @@ -18,6 +18,7 @@ from vitrage.evaluator.actions.recipes.base import ActionStepWrapper MISTRAL = 'mistral' +INPUT = 'input' WORKFLOW = 'workflow' diff --git a/vitrage/evaluator/scenario_evaluator.py b/vitrage/evaluator/scenario_evaluator.py index 8b201f69c..530023ea2 100644 --- a/vitrage/evaluator/scenario_evaluator.py +++ b/vitrage/evaluator/scenario_evaluator.py @@ -20,6 +20,7 @@ from oslo_log import log from vitrage.common.constants import EdgeProperties as EProps from vitrage.common.constants import VertexProperties as VProps +from vitrage.common.utils import recursive_keypairs from vitrage.datasources.listener_service import defaultdict from vitrage.entity_graph.mappings.datasource_info_mapper \ import DatasourceInfoMapper @@ -238,11 +239,19 @@ class ScenarioEvaluator(EvaluatorBase): @staticmethod def _generate_action_id(action_spec): + """Generate a unique action id for the action + + BEWARE: The implementation of this function MUST NOT BE CHANGED!! + + The created hash is used for storing the active actions in the + database. If changed, existing active actions can no longer be + retrieved. + """ targets = [(k, v.vertex_id) for k, v in action_spec.targets.items()] return hash( (action_spec.type, tuple(sorted(targets)), - tuple(sorted(action_spec.properties.items()))) + tuple(sorted(recursive_keypairs(action_spec.properties)))) ) def _analyze_and_filter_actions(self, actions): diff --git a/vitrage/evaluator/scenario_repository.py b/vitrage/evaluator/scenario_repository.py index efd793e39..9917f4ed7 100644 --- a/vitrage/evaluator/scenario_repository.py +++ b/vitrage/evaluator/scenario_repository.py @@ -25,8 +25,8 @@ from vitrage.evaluator.equivalence_repository import EquivalenceRepository from vitrage.evaluator.template_fields import TemplateFields from vitrage.evaluator.template_loading.scenario_loader import ScenarioLoader from vitrage.evaluator.template_loading.template_loader import TemplateLoader -from vitrage.evaluator.template_validation.content.definitions_validator \ - import DefinitionsValidator as DefValidator +from vitrage.evaluator.template_validation.content.base import \ + get_template_schema from vitrage.evaluator.template_validation.content.template_content_validator \ import content_validation from vitrage.evaluator.template_validation.template_syntax_validator import \ @@ -135,23 +135,30 @@ class ScenarioRepository(object): self._add_scenario(equivalent_scenario) def add_def_template(self, def_template): + result, template_schema = get_template_schema(def_template) - result = def_template_syntax_validation(def_template) - if not result.is_valid_config: - LOG.info('Unable to load definition template, syntax err: %s' - % result.comment) - else: - result = DefValidator.def_template_content_validation(def_template) + if result.is_valid_config: + result = def_template_syntax_validation(def_template) if not result.is_valid_config: - LOG.info('Unable to load definition template, content err: %s' + LOG.info('Unable to load definition template, syntax err: %s' % result.comment) - current_time = datetime_utils.utcnow() - include_uuid = uuidutils.generate_uuid() - self._def_templates[str(include_uuid)] = Template(include_uuid, - def_template, - current_time, - result) + if result.is_valid_config: + def_validator = \ + template_schema.validator(TemplateFields.DEFINITIONS) + result = \ + def_validator.def_template_content_validation(def_template) + + if result.is_valid_config: + current_time = datetime_utils.utcnow() + include_uuid = uuidutils.generate_uuid() + self._def_templates[str(include_uuid)] = Template(include_uuid, + def_template, + current_time, + result) + else: + LOG.info('Unable to load definition template, content err: %s' + % result.comment) def _expand_equivalence(self, scenario): equivalent_scenarios = [scenario] diff --git a/vitrage/evaluator/template_fields.py b/vitrage/evaluator/template_fields.py index 56bd8c415..f6ebd0aae 100644 --- a/vitrage/evaluator/template_fields.py +++ b/vitrage/evaluator/template_fields.py @@ -15,10 +15,6 @@ from vitrage.common.constants import TemplateTopologyFields -DEFAULT_VERSION = '1' -SUPPORTED_VERSIONS = ['1'] - - class TemplateFields(TemplateTopologyFields): SCENARIOS = 'scenarios' diff --git a/vitrage/evaluator/template_loading/scenario_loader.py b/vitrage/evaluator/template_loading/scenario_loader.py index 2b9e23474..d76404152 100644 --- a/vitrage/evaluator/template_loading/scenario_loader.py +++ b/vitrage/evaluator/template_loading/scenario_loader.py @@ -12,24 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log + from vitrage.common.exception import VitrageError from vitrage.evaluator.condition import get_condition_common_targets from vitrage.evaluator.condition import parse_condition from vitrage.evaluator.condition import SymbolResolver -from vitrage.evaluator.template_data import ActionSpecs from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_data import ENTITY from vitrage.evaluator.template_data import RELATIONSHIP from vitrage.evaluator.template_data import Scenario from vitrage.evaluator.template_fields import TemplateFields as TFields -from vitrage.evaluator.template_loading.subgraph_builder import SubGraphBuilder +from vitrage.evaluator.template_loading.subgraph_builder import \ + SubGraphBuilder from vitrage.graph import Vertex +LOG = log.getLogger(__name__) + class ScenarioLoader(object): - def __init__(self, name, entities, relationships): + def __init__(self, template_schema, name, entities, relationships): self.name = name + self._template_schema = template_schema self._template_entities = entities self._template_relationships = relationships @@ -93,14 +98,14 @@ class ScenarioLoader(object): for counter, action_def in enumerate(actions_def): action_id = '%s-action%s' % (scenario_id, str(counter)) - action_dict = action_def[TFields.ACTION] - action_type = action_dict[TFields.ACTION_TYPE] - targets = action_dict.get(TFields.ACTION_TARGET, - self.valid_target) - properties = action_dict.get(TFields.PROPERTIES, {}) + action_type = action_def[TFields.ACTION][TFields.ACTION_TYPE] + action_loader = self._template_schema.loader(action_type) - actions.append( - ActionSpecs(action_id, action_type, targets, properties)) + if action_loader: + actions.append(action_loader.load(action_id, self.valid_target, + action_def)) + else: + LOG.warning('Failed to load action of type %s', action_type) return actions diff --git a/vitrage/evaluator/template_loading/template_loader.py b/vitrage/evaluator/template_loading/template_loader.py index 943884ec4..0336fcec3 100644 --- a/vitrage/evaluator/template_loading/template_loader.py +++ b/vitrage/evaluator/template_loading/template_loader.py @@ -12,17 +12,23 @@ # License for the specific language governing permissions and limitations # under the License. +from oslo_log import log + from vitrage.common.constants import VertexProperties as VProps from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_data import TemplateData from vitrage.evaluator.template_fields import TemplateFields as TFields from vitrage.evaluator.template_loading.props_converter import PropsConverter from vitrage.evaluator.template_loading.scenario_loader import ScenarioLoader +from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory from vitrage.graph import Edge from vitrage.graph import Vertex from vitrage.utils import evaluator as evaluator_utils +LOG = log.getLogger(__name__) + + class TemplateLoader(object): PROPS_CONVERSION = { @@ -43,6 +49,12 @@ class TemplateLoader(object): self.relationships = {} def load(self, template_def, def_templates=None): + + template_schema = self._get_template_schema(template_def) + if not template_schema: + LOG.error('Failed to load template - unsupported version') + return + name = template_def[TFields.METADATA][TFields.NAME] if def_templates is None: @@ -70,8 +82,9 @@ class TemplateLoader(object): def_templates, self.relationships) - scenarios = ScenarioLoader(name, self.entities, self.relationships)\ - .build_scenarios(template_def[TFields.SCENARIOS]) + scenarios = ScenarioLoader(template_schema, name, self.entities, + self.relationships).\ + build_scenarios(template_def[TFields.SCENARIOS]) return TemplateData(name, self.entities, self.relationships, scenarios) @@ -162,3 +175,13 @@ class TemplateLoader(object): ignore_ids = [TFields.TEMPLATE_ID, TFields.SOURCE, TFields.TARGET] return \ {key: var_dict[key] for key in var_dict if key not in ignore_ids} + + @staticmethod + def _get_template_schema(template): + metadata = template.get(TFields.METADATA) + + if metadata: + version = metadata.get(TFields.VERSION) + return TemplateSchemaFactory().template_schema(version) + else: + return None diff --git a/vitrage/evaluator/template_loading/v1/__init__.py b/vitrage/evaluator/template_loading/v1/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage/evaluator/template_loading/v1/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__author__ = 'stack' diff --git a/vitrage/evaluator/template_loading/v1/action_loader.py b/vitrage/evaluator/template_loading/v1/action_loader.py new file mode 100644 index 000000000..5b96a0d82 --- /dev/null +++ b/vitrage/evaluator/template_loading/v1/action_loader.py @@ -0,0 +1,37 @@ +# Copyright 2016 - 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. + +import abc + +from vitrage.evaluator.template_data import ActionSpecs +from vitrage.evaluator.template_fields import TemplateFields as TFields + + +class BaseActionLoader(object): + + def load(self, action_id, valid_target, action_def): + action_dict = action_def[TFields.ACTION] + action_type = action_dict[TFields.ACTION_TYPE] + targets = action_dict.get(TFields.ACTION_TARGET, valid_target) + return ActionSpecs(action_id, action_type, targets, + self._get_properties(action_dict)) + + @abc.abstractmethod + def _get_properties(self, action_dict): + pass + + +class ActionLoader(BaseActionLoader): + def _get_properties(self, action_dict): + return action_dict.get(TFields.PROPERTIES, {}) diff --git a/vitrage/evaluator/template_loading/v1/execute_mistral_loader.py b/vitrage/evaluator/template_loading/v1/execute_mistral_loader.py new file mode 100644 index 000000000..0a402aef1 --- /dev/null +++ b/vitrage/evaluator/template_loading/v1/execute_mistral_loader.py @@ -0,0 +1,29 @@ +# 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 copy import deepcopy +from vitrage.evaluator.actions.recipes.execute_mistral import INPUT +from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW +from vitrage.evaluator.template_fields import TemplateFields as TFields +from vitrage.evaluator.template_loading.v1.action_loader import \ + BaseActionLoader + + +class ExecuteMistralLoader(BaseActionLoader): + def _get_properties(self, action_dict): + """Place all properties under an 'input' block""" + properties = action_dict.get(TFields.PROPERTIES, {}) + input_properties = deepcopy(properties) + input_properties.pop(WORKFLOW) + return {WORKFLOW: properties[WORKFLOW], INPUT: input_properties} diff --git a/vitrage/evaluator/template_schema_factory.py b/vitrage/evaluator/template_schema_factory.py new file mode 100644 index 000000000..6eacf970d --- /dev/null +++ b/vitrage/evaluator/template_schema_factory.py @@ -0,0 +1,42 @@ +# 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 + +LOG = log.getLogger(__name__) + + +class TemplateSchemaFactory(object): + + DEFAULT_VERSION = '1' + _schemas = dict() + + @classmethod + def supported_versions(cls): + return cls._schemas.keys() + + @classmethod + def is_version_supported(cls, version): + return version in cls._schemas + + @classmethod + def template_schema(cls, version): + if not version: + version = cls.DEFAULT_VERSION + template_schema = cls._schemas.get(version) + return template_schema + + @classmethod + def register_template_schema(cls, version, template_schema): + cls._schemas[version] = template_schema + LOG.debug('Registered template schema for version %s', version) diff --git a/vitrage/evaluator/template_schemas.py b/vitrage/evaluator/template_schemas.py new file mode 100644 index 000000000..986ea6890 --- /dev/null +++ b/vitrage/evaluator/template_schemas.py @@ -0,0 +1,86 @@ +# 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.base import ActionType +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator.template_loading.v1.action_loader import ActionLoader +from vitrage.evaluator.template_loading.v1.execute_mistral_loader import \ + ExecuteMistralLoader +from vitrage.evaluator.template_validation.content.v1.\ + add_causal_relationship_validator import AddCausalRelationshipValidator +from vitrage.evaluator.template_validation.content.v1.definitions_validator \ + import DefinitionsValidator +from vitrage.evaluator.template_validation.content.v1.\ + execute_mistral_validator import ExecuteMistralValidator as \ + V1ExecuteMistralValidator +from vitrage.evaluator.template_validation.content.v1.mark_down_validator \ + import MarkDownValidator +from vitrage.evaluator.template_validation.content.v1.raise_alarm_validator \ + import RaiseAlarmValidator +from vitrage.evaluator.template_validation.content.v1.scenario_validator \ + import ScenarioValidator +from vitrage.evaluator.template_validation.content.v1.set_state_validator \ + import SetStateValidator +from vitrage.evaluator.template_validation.content.v2.\ + execute_mistral_validator import ExecuteMistralValidator as \ + V2ExecuteMistralValidator +from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory + +LOG = log.getLogger(__name__) + + +class TemplateSchema1(object): + def __init__(self): + self._validators = { + TemplateFields.DEFINITIONS: DefinitionsValidator, + TemplateFields.SCENARIOS: ScenarioValidator, + ActionType.ADD_CAUSAL_RELATIONSHIP: AddCausalRelationshipValidator, + ActionType.EXECUTE_MISTRAL: V1ExecuteMistralValidator, + ActionType.MARK_DOWN: MarkDownValidator, + ActionType.RAISE_ALARM: RaiseAlarmValidator, + ActionType.SET_STATE: SetStateValidator, + } + + self._loaders = { + ActionType.ADD_CAUSAL_RELATIONSHIP: ActionLoader(), + ActionType.EXECUTE_MISTRAL: ExecuteMistralLoader(), + ActionType.MARK_DOWN: ActionLoader(), + ActionType.RAISE_ALARM: ActionLoader(), + 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) + + def loader(self, loader_type): + LOG.debug('Get loader. loader_type: %s. loaders: %s', + loader_type, self._loaders) + return self._loaders.get(loader_type) + + +class TemplateSchema2(TemplateSchema1): + + def __init__(self): + super(TemplateSchema2, self).__init__() + self._validators[ActionType.EXECUTE_MISTRAL] = \ + V2ExecuteMistralValidator() + self._loaders[ActionType.EXECUTE_MISTRAL] = ActionLoader() + + +def init_template_schemas(): + TemplateSchemaFactory.register_template_schema('1', TemplateSchema1()) + TemplateSchemaFactory.register_template_schema('2', TemplateSchema2()) diff --git a/vitrage/evaluator/template_validation/content/base.py b/vitrage/evaluator/template_validation/content/base.py index 96d0a6a79..6db2cca94 100644 --- a/vitrage/evaluator/template_validation/content/base.py +++ b/vitrage/evaluator/template_validation/content/base.py @@ -16,6 +16,8 @@ import abc import six from oslo_log import log +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.status_messages import status_msgs @@ -42,6 +44,23 @@ def validate_template_id(definitions_index, id_to_check): return get_correct_result(RESULT_DESCRIPTION) +def get_template_schema(template): + metadata = template.get(TemplateFields.METADATA) + + if metadata is None: + LOG.error('%s status code: %s' % (status_msgs[62], 62)) + return get_content_fault_result(62), None + + version = metadata.get(TemplateFields.VERSION) + template_schema = TemplateSchemaFactory().template_schema(version) + + if template_schema: + return get_content_correct_result(), template_schema + else: + LOG.error('%s status code: %s' % (status_msgs[63], 63)) + return get_content_fault_result(63), None + + @six.add_metaclass(abc.ABCMeta) class ActionValidator(object): diff --git a/vitrage/evaluator/template_validation/content/template_content_validator.py b/vitrage/evaluator/template_validation/content/template_content_validator.py index 954710028..e8562b88c 100644 --- a/vitrage/evaluator/template_validation/content/template_content_validator.py +++ b/vitrage/evaluator/template_validation/content/template_content_validator.py @@ -14,18 +14,9 @@ from oslo_log import log -from vitrage.evaluator.template_fields import DEFAULT_VERSION -from vitrage.evaluator.template_fields import SUPPORTED_VERSIONS from vitrage.evaluator.template_fields import TemplateFields 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.definitions_validator \ - import DefinitionsValidator as DefValidator -from vitrage.evaluator.template_validation.content.scenario_validator import \ - ScenarioValidator -from vitrage.evaluator.template_validation.status_messages import status_msgs + get_template_schema LOG = log.getLogger(__name__) @@ -35,65 +26,57 @@ def content_validation(template, def_templates=None): if def_templates is None: def_templates = {} - result = _validate_version(template) - entities_index = {} template_definitions = {} + result, template_schema = get_template_schema(template) + def_validator = template_schema.validator(TemplateFields.DEFINITIONS) \ + if result.is_valid_config and template_schema else None + + if result.is_valid_config and not def_validator: + result.is_valid_config = False # Not supposed to happen + if result.is_valid_config and TemplateFields.DEFINITIONS in template: template_definitions = template[TemplateFields.DEFINITIONS] if TemplateFields.ENTITIES in template_definitions: entities = template_definitions[TemplateFields.ENTITIES] - result = DefValidator.validate_entities_definition(entities, - entities_index) + result = def_validator.validate_entities_definition(entities, + entities_index) # If there are duplicate definitions in several includes under the same # name, will regard the first one if result.is_valid_config and TemplateFields.INCLUDES in template: template_includes = template[TemplateFields.INCLUDES] result = \ - DefValidator.validate_definitions_with_includes(template_includes, - def_templates, - entities_index) + def_validator.validate_definitions_with_includes(template_includes, + def_templates, + entities_index) relationship_index = {} if result.is_valid_config and \ TemplateFields.RELATIONSHIPS in template_definitions: relationships = template_definitions[TemplateFields.RELATIONSHIPS] - result = \ - DefValidator.validate_relationships_definitions(relationships, - relationship_index, - entities_index) + result = def_validator.validate_relationships_definitions( + relationships, relationship_index, entities_index) if result.is_valid_config and TemplateFields.INCLUDES in template: template_includes = template[TemplateFields.INCLUDES] - result = DefValidator.validate_relationships_definitions_with_includes( - template_includes, - def_templates, - entities_index, - relationship_index) + result = \ + def_validator.validate_relationships_definitions_with_includes( + template_includes, + def_templates, + entities_index, + relationship_index) if result.is_valid_config: + scenario_validator = template_schema.validator( + TemplateFields.SCENARIOS) scenarios = template[TemplateFields.SCENARIOS] definitions_index = entities_index.copy() definitions_index.update(relationship_index) - result = ScenarioValidator(definitions_index).validate(scenarios) + result = scenario_validator.validate(template_schema, + definitions_index, scenarios) return result - - -def _validate_version(template): - metadata = template.get(TemplateFields.METADATA) - - if metadata is None: - LOG.error('%s status code: %s' % (status_msgs[62], 62)) - return get_content_fault_result(62) - - version = metadata.get(TemplateFields.VERSION, DEFAULT_VERSION) - if version in SUPPORTED_VERSIONS: - return get_content_correct_result() - else: - LOG.error('%s status code: %s' % (status_msgs[63], 63)) - return get_content_fault_result(63) diff --git a/vitrage/evaluator/template_validation/content/v1/__init__.py b/vitrage/evaluator/template_validation/content/v1/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage/evaluator/template_validation/content/v1/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__author__ = 'stack' diff --git a/vitrage/evaluator/template_validation/content/add_causal_relationship_validator.py b/vitrage/evaluator/template_validation/content/v1/add_causal_relationship_validator.py similarity index 100% rename from vitrage/evaluator/template_validation/content/add_causal_relationship_validator.py rename to vitrage/evaluator/template_validation/content/v1/add_causal_relationship_validator.py diff --git a/vitrage/evaluator/template_validation/content/definitions_validator.py b/vitrage/evaluator/template_validation/content/v1/definitions_validator.py similarity index 99% rename from vitrage/evaluator/template_validation/content/definitions_validator.py rename to vitrage/evaluator/template_validation/content/v1/definitions_validator.py index 663d35f89..52e468b26 100644 --- a/vitrage/evaluator/template_validation/content/definitions_validator.py +++ b/vitrage/evaluator/template_validation/content/v1/definitions_validator.py @@ -143,6 +143,7 @@ class DefinitionsValidator(object): return get_content_correct_result() + # noinspection PyBroadException @classmethod def _validate_entity_definition(cls, entity_dict, entities_index): template_id = entity_dict[TemplateFields.TEMPLATE_ID] diff --git a/vitrage/evaluator/template_validation/content/execute_mistral_validator.py b/vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py similarity index 100% rename from vitrage/evaluator/template_validation/content/execute_mistral_validator.py rename to vitrage/evaluator/template_validation/content/v1/execute_mistral_validator.py diff --git a/vitrage/evaluator/template_validation/content/mark_down_validator.py b/vitrage/evaluator/template_validation/content/v1/mark_down_validator.py similarity index 100% rename from vitrage/evaluator/template_validation/content/mark_down_validator.py rename to vitrage/evaluator/template_validation/content/v1/mark_down_validator.py diff --git a/vitrage/evaluator/template_validation/content/raise_alarm_validator.py b/vitrage/evaluator/template_validation/content/v1/raise_alarm_validator.py similarity index 100% rename from vitrage/evaluator/template_validation/content/raise_alarm_validator.py rename to vitrage/evaluator/template_validation/content/v1/raise_alarm_validator.py diff --git a/vitrage/evaluator/template_validation/content/scenario_validator.py b/vitrage/evaluator/template_validation/content/v1/scenario_validator.py similarity index 68% rename from vitrage/evaluator/template_validation/content/scenario_validator.py rename to vitrage/evaluator/template_validation/content/v1/scenario_validator.py index c5ec5bd1f..254246f2b 100644 --- a/vitrage/evaluator/template_validation/content/scenario_validator.py +++ b/vitrage/evaluator/template_validation/content/v1/scenario_validator.py @@ -19,29 +19,18 @@ from oslo_log import log from six.moves import reduce from vitrage.common.constants import EdgeProperties as EProps -from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.condition import convert_to_dnf_format from vitrage.evaluator.condition import get_condition_common_targets from vitrage.evaluator.condition import is_condition_include_positive_clause from vitrage.evaluator.condition import parse_condition from vitrage.evaluator.condition import SymbolResolver from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.content. \ - add_causal_relationship_validator import AddCausalRelationshipValidator 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 \ validate_template_id -from vitrage.evaluator.template_validation.content.execute_mistral_validator \ - import ExecuteMistralValidator -from vitrage.evaluator.template_validation.content.mark_down_validator \ - import MarkDownValidator -from vitrage.evaluator.template_validation.content.raise_alarm_validator \ - import RaiseAlarmValidator -from vitrage.evaluator.template_validation.content.set_state_validator \ - import SetStateValidator from vitrage.evaluator.template_validation.status_messages import status_msgs LOG = log.getLogger(__name__) @@ -62,28 +51,29 @@ class ScenarioValidator(object): def get_entity_id(self, entity): return entity[TemplateFields.TEMPLATE_ID] - def __init__(self, definitions_index): - self.definitions_index = definitions_index - - def validate(self, scenarios): + @classmethod + def validate(cls, template_schema, def_index, scenarios): for scenario in scenarios: scenario_values = scenario[TemplateFields.SCENARIO] condition = scenario_values[TemplateFields.CONDITION] - result = self._validate_scenario_condition(condition) + result = cls._validate_scenario_condition(def_index, condition) if not result.is_valid_config: return result actions = scenario_values[TemplateFields.ACTIONS] - result = self._validate_scenario_actions(actions) + result = cls._validate_scenario_actions(template_schema, + def_index, actions) if not result.is_valid_config: return result return get_content_correct_result() - def _validate_scenario_condition(self, condition): + # noinspection PyBroadException + @classmethod + def _validate_scenario_condition(cls, def_index, condition): try: dnf_result = convert_to_dnf_format(condition) except Exception: @@ -91,7 +81,8 @@ class ScenarioValidator(object): return get_content_fault_result(85) # not condition validation - not_condition_result = self._validate_not_condition(dnf_result) + not_condition_result = cls._validate_not_condition(def_index, + dnf_result) if not not_condition_result.is_valid_config: return not_condition_result @@ -107,28 +98,29 @@ class ScenarioValidator(object): continue result = \ - validate_template_id(self.definitions_index, condition_var) + validate_template_id(def_index, condition_var) if not result.is_valid_config: return result # condition structure validation - condition_structure_result = \ - self._validate_condition_structure(parse_condition(condition)) + condition_structure_result = cls._validate_condition_structure( + def_index, parse_condition(condition)) if not condition_structure_result.is_valid_config: return condition_structure_result return get_content_correct_result() - def _validate_condition_structure(self, condition_dnf): + @classmethod + def _validate_condition_structure(cls, def_index, condition_dnf): result = \ - self._validate_condition_includes_positive_clause(condition_dnf) + cls._validate_condition_includes_positive_clause(condition_dnf) if not result.is_valid_config: return result common_targets = \ get_condition_common_targets(condition_dnf, - self.definitions_index, - self.TemplateSymbolResolver()) + def_index, + cls.TemplateSymbolResolver()) return get_content_correct_result() if common_targets \ else get_content_fault_result(135) @@ -139,65 +131,63 @@ class ScenarioValidator(object): is_condition_include_positive_clause(condition) \ else get_content_fault_result(134) - def _validate_not_condition(self, dnf_result): + @classmethod + def _validate_not_condition(cls, def_index, dnf_result): """Not operator validation Not operator can appear only on edges. :param dnf_result: - :param definitions_index: + :param def_index: :return: """ if isinstance(dnf_result, Not): for arg in dnf_result.args: if isinstance(arg, Symbol): - definition = self.definitions_index.get(str(arg), None) + definition = def_index.get(str(arg), None) if not (definition and definition.get(EProps.RELATIONSHIP_TYPE)): msg = status_msgs[86] + ' template id: %s' % arg LOG.error('%s status code: %s' % (msg, 86)) return get_content_fault_result(86, msg) else: - res = self._validate_not_condition(arg) + res = cls._validate_not_condition(def_index, arg) if not res.is_valid_config: return res return get_content_correct_result() for arg in dnf_result.args: if not isinstance(arg, Symbol): - res = self._validate_not_condition(arg) + res = cls._validate_not_condition(def_index, arg) if not res.is_valid_config: return res return get_content_correct_result() - def _validate_scenario_actions(self, actions): + @classmethod + def _validate_scenario_actions(cls, + template_schema, + def_index, + actions): for action in actions: result = \ - self._validate_scenario_action(action[TemplateFields.ACTION]) + cls._validate_scenario_action(template_schema, + def_index, + action[TemplateFields.ACTION]) if not result.is_valid_config: return result return get_content_correct_result() - def _validate_scenario_action(self, action): - + @staticmethod + def _validate_scenario_action(template_schema, def_index, action): action_type = action[TemplateFields.ACTION_TYPE] + action_validator = template_schema.validator(action_type) - action_validators = { - ActionType.RAISE_ALARM: RaiseAlarmValidator(), - ActionType.SET_STATE: SetStateValidator(), - ActionType.ADD_CAUSAL_RELATIONSHIP: - AddCausalRelationshipValidator(), - ActionType.MARK_DOWN: MarkDownValidator(), - ActionType.EXECUTE_MISTRAL: ExecuteMistralValidator(), - } - - if action_type not in action_validators: + if not action_validator: LOG.error('%s status code: %s' % (status_msgs[120], 120)) return get_content_fault_result(120) - return action_validators[action_type].validate(action, - self.definitions_index) + return action_validator.validate(action, def_index) diff --git a/vitrage/evaluator/template_validation/content/set_state_validator.py b/vitrage/evaluator/template_validation/content/v1/set_state_validator.py similarity index 100% rename from vitrage/evaluator/template_validation/content/set_state_validator.py rename to vitrage/evaluator/template_validation/content/v1/set_state_validator.py diff --git a/vitrage/evaluator/template_validation/content/v2/__init__.py b/vitrage/evaluator/template_validation/content/v2/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage/evaluator/template_validation/content/v2/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__author__ = 'stack' diff --git a/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py b/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py new file mode 100644 index 000000000..3f0f5b60e --- /dev/null +++ b/vitrage/evaluator/template_validation/content/v2/execute_mistral_validator.py @@ -0,0 +1,47 @@ +# 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 INPUT +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) + + for prop in properties: + if prop not in {WORKFLOW, INPUT}: + LOG.error('%s status code: %s' % (status_msgs[136], 136)) + return get_content_fault_result(136) + + return get_content_correct_result() diff --git a/vitrage/evaluator/template_validation/status_messages.py b/vitrage/evaluator/template_validation/status_messages.py index 407bca5a4..3925fc9f5 100644 --- a/vitrage/evaluator/template_validation/status_messages.py +++ b/vitrage/evaluator/template_validation/status_messages.py @@ -14,7 +14,8 @@ from vitrage.common.constants import EdgeLabel from vitrage.common.constants import EntityCategory from vitrage.evaluator.actions.base import action_types -from vitrage.evaluator.template_fields import SUPPORTED_VERSIONS +from vitrage.evaluator.template_schema_factory import TemplateSchemaFactory + status_msgs = { @@ -43,7 +44,7 @@ status_msgs = { 60: 'metadata section must contain id field.', 62: 'metadata is a mandatory section.', 63: 'Unsupported version. Version must be one of: {versions}' - .format(versions=SUPPORTED_VERSIONS), + .format(versions=TemplateSchemaFactory.supported_versions()), # scenarios section status messages 80-99 80: 'scenarios is a mandatory section.', @@ -84,6 +85,8 @@ status_msgs = { 'block', 134: 'condition can not contain only \'not\' clauses', 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 ', # def_templates status messages 140-159 140: 'At least one template must be included', diff --git a/vitrage/notifier/plugins/mistral/mistral_notifier.py b/vitrage/notifier/plugins/mistral/mistral_notifier.py index bd8089375..9a93ed1db 100644 --- a/vitrage/notifier/plugins/mistral/mistral_notifier.py +++ b/vitrage/notifier/plugins/mistral/mistral_notifier.py @@ -14,6 +14,7 @@ from oslo_log import log as logging from vitrage.common.constants import NotifierEventTypes +from vitrage.evaluator.actions.recipes.execute_mistral import INPUT from vitrage.evaluator.actions.recipes.execute_mistral import WORKFLOW from vitrage.notifier.plugins.base import NotifierBase from vitrage import os_clients @@ -63,11 +64,11 @@ class MistralNotifier(NotifierBase): try: workflow = data[WORKFLOW] - del data[WORKFLOW] + workflow_input = data.get(INPUT, {}) response = self.client.executions.create( workflow_identifier=workflow, - workflow_input=data, + workflow_input=workflow_input, wf_params={}) if response: diff --git a/vitrage_tempest_tests/tests/resources/templates/api/execute_mistral.yaml b/vitrage/tests/resources/templates/version/v1_execute_mistral.yaml similarity index 69% rename from vitrage_tempest_tests/tests/resources/templates/api/execute_mistral.yaml rename to vitrage/tests/resources/templates/version/v1_execute_mistral.yaml index b180d7565..94c1c645a 100644 --- a/vitrage_tempest_tests/tests/resources/templates/api/execute_mistral.yaml +++ b/vitrage/tests/resources/templates/version/v1_execute_mistral.yaml @@ -1,25 +1,26 @@ metadata: - name: execute_mistral + version: 1 + name: v1_execute_mistral description: execute mistral definitions: entities: - entity: category: ALARM - name: compute.host.down - template_id: host_down_alarm + name: notifiers.mistral.trigger.alarm.1 + template_id: alarm - entity: category: RESOURCE type: nova.host template_id: host relationships: - relationship: - source: host_down_alarm + source: alarm relationship_type: on target: host - template_id : host_down_alarm_on_host + template_id : alarm_on_host scenarios: - scenario: - condition: host_down_alarm_on_host + condition: alarm_on_host actions: - action: action_type: execute_mistral diff --git a/vitrage/tests/resources/templates/version/v2_execute_mistral.yaml b/vitrage/tests/resources/templates/version/v2_execute_mistral.yaml new file mode 100644 index 000000000..60585c334 --- /dev/null +++ b/vitrage/tests/resources/templates/version/v2_execute_mistral.yaml @@ -0,0 +1,30 @@ +metadata: + version: 2 + name: v2_execute_mistral + description: execute mistral +definitions: + entities: + - entity: + category: ALARM + name: notifiers.mistral.trigger.alarm.2 + 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: Hello and Goodbye diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py similarity index 57% rename from vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py rename to vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py index 640acdaa2..6ca92c4d5 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_execute_mistral_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/base_test_execute_mistral_validator.py @@ -11,28 +11,30 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import abc 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): +class BaseExecuteMistralValidatorTest(ActionValidatorTest): - def test_validate_execute_mistral_action(self): + @abc.abstractmethod + def _create_execute_mistral_action(self, workflow, host, host_state): + pass + def _validate_execute_mistral_action(self, validator): self._validate_action( self._create_execute_mistral_action('wf_1', 'host_2', 'down'), - ExecuteMistralValidator.validate + validator.validate ) - def test_validate_execute_mistral_action_without_workflow(self): + def _validate_execute_mistral_action_without_workflow(self, validator): # Test setup idx = DEFINITIONS_INDEX_MOCK.copy() @@ -40,51 +42,63 @@ class ExecuteMistralValidatorTest(ActionValidatorTest): action[TemplateFields.PROPERTIES].pop(WORKFLOW) # Test action - result = ExecuteMistralValidator.validate(action, idx) + result = validator.validate(action, idx) # Test assertions self._assert_fault_result(result, 133) - def test_validate_execute_mistral_action_with_empty_workflow(self): + def _validate_execute_mistral_action_with_empty_workflow(self, validator): # Test setup idx = DEFINITIONS_INDEX_MOCK.copy() action = self._create_execute_mistral_action('', 'host_2', 'down') # Test action - result = ExecuteMistralValidator.validate(action, idx) + result = validator.validate(action, idx) # Test assertions self._assert_fault_result(result, 133) - def test_validate_execute_mistral_action_with_none_workflow(self): + def _validate_execute_mistral_action_with_none_workflow(self, validator): # Test setup idx = DEFINITIONS_INDEX_MOCK.copy() action = self._create_execute_mistral_action(None, 'host_2', 'down') # Test action - result = ExecuteMistralValidator.validate(action, idx) + result = validator.validate(action, idx) # Test assertions self._assert_fault_result(result, 133) - def test_validate_execute_mistral_action_without_additional_params(self): + def _validate_execute_mistral_action_without_additional_props(self, + validator): # 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') + action = self._create_no_input_mistral_action('wf_1') # Test action - result = ExecuteMistralValidator.validate(action, idx) + result = validator.validate(action, idx) # Test assertions self._assert_correct_result(result) @staticmethod - def _create_execute_mistral_action(workflow, host, host_state): + def _create_no_input_mistral_action(workflow): + + properties = { + WORKFLOW: workflow, + } + action = { + TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL, + TemplateFields.PROPERTIES: properties + } + + return action + + @staticmethod + def _create_v1_execute_mistral_action(workflow, host, host_state): properties = { WORKFLOW: workflow, @@ -97,3 +111,21 @@ class ExecuteMistralValidatorTest(ActionValidatorTest): } return action + + @staticmethod + def _create_v2_execute_mistral_action(workflow, host, host_state): + + input_props = { + 'host': host, + 'host_state': host_state + } + properties = { + WORKFLOW: workflow, + 'input': input_props + } + action = { + TemplateFields.ACTION_TYPE: ActionType.EXECUTE_MISTRAL, + TemplateFields.PROPERTIES: properties + } + + return action diff --git a/vitrage/tests/unit/evaluator/template_validation/content/v1/__init__.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__author__ = 'stack' diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_add_causal_relationship_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_add_causal_relationship_validator.py similarity index 98% rename from vitrage/tests/unit/evaluator/template_validation/content/test_add_causal_relationship_validator.py rename to vitrage/tests/unit/evaluator/template_validation/content/v1/test_add_causal_relationship_validator.py index 7aa943623..6dbfd4a68 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_add_causal_relationship_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_add_causal_relationship_validator.py @@ -14,7 +14,7 @@ from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.content. \ +from vitrage.evaluator.template_validation.content.v1.\ add_causal_relationship_validator import AddCausalRelationshipValidator from vitrage.tests.unit.evaluator.template_validation.content.base import \ ActionValidatorTest 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 new file mode 100644 index 000000000..2ea11aa5d --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_execute_mistral_validator.py @@ -0,0 +1,65 @@ +# 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.template_fields import TemplateFields +from vitrage.evaluator.template_validation.content.v1.\ + execute_mistral_validator import ExecuteMistralValidator as \ + V1ExecuteMistralValidator +from vitrage.tests.unit.evaluator.template_validation.content.\ + base_test_execute_mistral_validator import BaseExecuteMistralValidatorTest +from vitrage.tests.unit.evaluator.template_validation.content.base import \ + DEFINITIONS_INDEX_MOCK + + +class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest): + + @classmethod + def setUpClass(cls): + super(ExecuteMistralValidatorTest, cls).setUpClass() + cls.validator = V1ExecuteMistralValidator() + + def test_v1_validate_execute_mistral_action(self): + self._validate_execute_mistral_action(self.validator) + + def test_validate_execute_mistral_action_without_workflow(self): + self._validate_execute_mistral_action_without_workflow(self.validator) + + def test_validate_execute_mistral_action_with_empty_workflow(self): + self._validate_execute_mistral_action_with_empty_workflow( + self.validator) + + def test_validate_execute_mistral_action_with_none_workflow(self): + self._validate_execute_mistral_action_with_none_workflow( + self.validator) + + def test_validate_execute_mistral_action_without_additional_props(self): + self._validate_execute_mistral_action_without_additional_props( + self.validator) + + def test_validate_execute_mistral_action_with_input_prop(self): + """A version1 execute_mistral action can have an 'input' property""" + + # Test setup + idx = DEFINITIONS_INDEX_MOCK.copy() + action = self._create_execute_mistral_action('wf_1', 'host_2', 'down') + action[TemplateFields.PROPERTIES]['input'] = 'kuku' + + # Test action + result = self.validator.validate(action, idx) + + # Test assertions + self._assert_correct_result(result) + + 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/test_mark_down_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_mark_down_validator.py similarity index 96% rename from vitrage/tests/unit/evaluator/template_validation/content/test_mark_down_validator.py rename to vitrage/tests/unit/evaluator/template_validation/content/v1/test_mark_down_validator.py index c2c910ca2..d8bf30030 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_mark_down_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_mark_down_validator.py @@ -14,7 +14,7 @@ from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.content.mark_down_validator import \ +from vitrage.evaluator.template_validation.content.v1.mark_down_validator import \ MarkDownValidator from vitrage.tests.unit.evaluator.template_validation.content.base import \ ActionValidatorTest diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_raise_alarm_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_raise_alarm_validator.py similarity index 97% rename from vitrage/tests/unit/evaluator/template_validation/content/test_raise_alarm_validator.py rename to vitrage/tests/unit/evaluator/template_validation/content/v1/test_raise_alarm_validator.py index 18e6e650f..a139e478e 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_raise_alarm_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_raise_alarm_validator.py @@ -14,7 +14,7 @@ from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.content.raise_alarm_validator \ +from vitrage.evaluator.template_validation.content.v1.raise_alarm_validator \ import RaiseAlarmValidator from vitrage.tests.unit.evaluator.template_validation.content.base import \ ActionValidatorTest diff --git a/vitrage/tests/unit/evaluator/template_validation/content/test_set_state_validator.py b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_set_state_validator.py similarity index 97% rename from vitrage/tests/unit/evaluator/template_validation/content/test_set_state_validator.py rename to vitrage/tests/unit/evaluator/template_validation/content/v1/test_set_state_validator.py index e9818e055..abf876ddd 100644 --- a/vitrage/tests/unit/evaluator/template_validation/content/test_set_state_validator.py +++ b/vitrage/tests/unit/evaluator/template_validation/content/v1/test_set_state_validator.py @@ -16,7 +16,7 @@ from vitrage.entity_graph.mappings.operational_resource_state import \ OperationalResourceState from vitrage.evaluator.actions.base import ActionType from vitrage.evaluator.template_fields import TemplateFields -from vitrage.evaluator.template_validation.content.set_state_validator \ +from vitrage.evaluator.template_validation.content.v1.set_state_validator \ import SetStateValidator from vitrage.tests.unit.evaluator.template_validation.content.base import \ ActionValidatorTest diff --git a/vitrage/tests/unit/evaluator/template_validation/content/v2/__init__.py b/vitrage/tests/unit/evaluator/template_validation/content/v2/__init__.py new file mode 100644 index 000000000..bf9f61d74 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/content/v2/__init__.py @@ -0,0 +1,15 @@ +# 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. + +__author__ = 'stack' 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 new file mode 100644 index 000000000..fb21cc608 --- /dev/null +++ b/vitrage/tests/unit/evaluator/template_validation/content/v2/test_execute_mistral_validator.py @@ -0,0 +1,68 @@ +# 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.template_validation.content.v2.\ + execute_mistral_validator import ExecuteMistralValidator as \ + V2ExecuteMistralValidator +from vitrage.tests.unit.evaluator.template_validation.content.\ + base_test_execute_mistral_validator import BaseExecuteMistralValidatorTest +from vitrage.tests.unit.evaluator.template_validation.content.base import \ + DEFINITIONS_INDEX_MOCK + + +class ExecuteMistralValidatorTest(BaseExecuteMistralValidatorTest): + + @classmethod + def setUpClass(cls): + super(ExecuteMistralValidatorTest, cls).setUpClass() + cls.validator = V2ExecuteMistralValidator() + + def test_v2_validate_execute_mistral_action(self): + self._validate_execute_mistral_action(self.validator) + + def test_v2_validate_old_execute_mistral_action(self): + """Test version2 validator on version1 template. + + An execute_mistral action from version 1 should fail in the validation + of version 2. + """ + + # Test setup + idx = DEFINITIONS_INDEX_MOCK.copy() + v1_action = \ + self._create_v1_execute_mistral_action('wf_1', 'host_2', 'down') + + # Test action + result = self.validator.validate(v1_action, idx) + + # Test assertions + self._assert_fault_result(result, 136) + + def test_validate_execute_mistral_action_without_workflow(self): + self._validate_execute_mistral_action_without_workflow(self.validator) + + def test_validate_execute_mistral_action_with_empty_workflow(self): + self._validate_execute_mistral_action_with_empty_workflow( + self.validator) + + def test_validate_execute_mistral_action_with_none_workflow(self): + self._validate_execute_mistral_action_with_none_workflow( + self.validator) + + def test_validate_execute_mistral_action_without_additional_props(self): + self._validate_execute_mistral_action_without_additional_props( + self.validator) + + 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/test_condition.py b/vitrage/tests/unit/evaluator/test_condition.py index a7e2ad4c0..6f79541b7 100644 --- a/vitrage/tests/unit/evaluator/test_condition.py +++ b/vitrage/tests/unit/evaluator/test_condition.py @@ -15,7 +15,7 @@ from vitrage.evaluator.condition import SymbolResolver from vitrage.evaluator.template_data import EdgeDescription from vitrage.evaluator.template_loading.template_loader import TemplateLoader -from vitrage.evaluator.template_validation.content.scenario_validator \ +from vitrage.evaluator.template_validation.content.v1.scenario_validator \ import get_condition_common_targets from vitrage.tests import base from vitrage.tests.mocks import utils diff --git a/vitrage/tests/unit/evaluator/test_scenario_evaluator.py b/vitrage/tests/unit/evaluator/test_scenario_evaluator.py new file mode 100644 index 000000000..4d41bf63e --- /dev/null +++ b/vitrage/tests/unit/evaluator/test_scenario_evaluator.py @@ -0,0 +1,48 @@ +# Copyright 2016 - 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.scenario_evaluator import ScenarioEvaluator +from vitrage.evaluator.template_data import ActionSpecs +from vitrage.tests import base + + +class TestScenarioEvaluator(base.BaseTest): + + def test_verify_execute_mistral_v2_action_hash(self): + execute_mistral_action_spec_1 = \ + ActionSpecs(id='mistmistmist1', + type=ActionType.EXECUTE_MISTRAL, + targets={}, + properties={'workflow': 'wf4', + 'input': { + 'prop1': 'ppp', + 'prop2': 'qqq', + 'prop3': 'rrr', + }}) + + execute_mistral_action_spec_2 = \ + ActionSpecs(id='mistmistmist2', + type=ActionType.EXECUTE_MISTRAL, + targets={}, + properties={'workflow': 'wf4', + 'input': { + 'prop2': 'qqq', + 'prop3': 'rrr', + 'prop1': 'ppp', + }}) + + self.assertEqual(ScenarioEvaluator. + _generate_action_id(execute_mistral_action_spec_1), + ScenarioEvaluator. + _generate_action_id(execute_mistral_action_spec_2)) diff --git a/vitrage/tests/unit/evaluator/test_template_loader.py b/vitrage/tests/unit/evaluator/test_template_loader.py index d1548a54a..3f37c2843 100644 --- a/vitrage/tests/unit/evaluator/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/test_template_loader.py @@ -38,6 +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' DEF_TEMPLATE_TESTS_DIR = utils.get_resources_dir() +\ '/templates/def_template_tests' @@ -230,6 +232,39 @@ class BasicTemplateTest(base.BaseTest): expected_relationships, expected_scenario) + def test_convert_v1_template(self): + # Load v1 and v2 templates, and get their actions + v1_action = self._get_template_single_action(self.V1_MISTRAL_TEMPLATE) + v2_action = self._get_template_single_action(self.V2_MISTRAL_TEMPLATE) + + # Validate that the action definition is identical (since v1 template + # should be converted to v2 format) + self._assert_equal_actions(v1_action, v2_action) + + def _get_template_single_action(self, template_file): + template_path = '%s/templates/version/%s' % (utils.get_resources_dir(), + template_file) + template_definition = file_utils.load_yaml_file(template_path, True) + template_data = TemplateLoader().load(template_definition) + scenarios = template_data.scenarios + self.assertIsNotNone(scenarios, 'Template should include a scenario') + self.assertEqual(1, len(scenarios), + 'Template should include a single scenario') + actions = scenarios[0].actions + self.assertIsNotNone(actions, 'Scenario should include an action') + self.assertEqual(1, len(actions), + 'Scenario should include a single action') + return actions[0] + + def _assert_equal_actions(self, action1, action2): + """Compare all action fields except from the id""" + self.assertEqual(action1.type, action2.type, + 'Action types should be equal') + self.assert_dict_equal(action1.targets, action2.targets, + 'Action targets should be equal') + self.assert_dict_equal(action1.properties, action2.properties, + 'Action targets should be equal') + def _validate_strict_equal(self, template_data, expected_entities, diff --git a/vitrage_tempest_tests/tests/api/event/base.py b/vitrage_tempest_tests/tests/api/event/base.py index ea9bdb838..86f050867 100644 --- a/vitrage_tempest_tests/tests/api/event/base.py +++ b/vitrage_tempest_tests/tests/api/event/base.py @@ -14,14 +14,13 @@ from datetime import datetime from oslo_log import log as logging -from vitrage_tempest_tests.tests.base import BaseVitrageTempest + +from vitrage_tempest_tests.tests.e2e.test_basic_actions import TestActionsBase LOG = logging.getLogger(__name__) -DOWN = 'down' -UP = 'up' -class BaseTestEvents(BaseVitrageTempest): +class BaseTestEvents(TestActionsBase): """Test class for Vitrage event API""" # noinspection PyPep8Naming diff --git a/vitrage_tempest_tests/tests/api/event/test_events.py b/vitrage_tempest_tests/tests/api/event/test_events.py index 3a6fc7dc3..7ddea5b3c 100644 --- a/vitrage_tempest_tests/tests/api/event/test_events.py +++ b/vitrage_tempest_tests/tests/api/event/test_events.py @@ -21,7 +21,7 @@ from vitrage.common.constants import EntityCategory from vitrage.common.constants import EventProperties as EventProps from vitrage.common.constants import VertexProperties as VProps from vitrage_tempest_tests.tests.api.event.base import BaseTestEvents -from vitrage_tempest_tests.tests.api.event.base import DOWN +from vitrage_tempest_tests.tests.common.vitrage_utils import DOWN from vitrage_tempest_tests.tests.utils import wait_for_answer diff --git a/vitrage_tempest_tests/tests/common/vitrage_utils.py b/vitrage_tempest_tests/tests/common/vitrage_utils.py index 4610799ea..d016b85b3 100644 --- a/vitrage_tempest_tests/tests/common/vitrage_utils.py +++ b/vitrage_tempest_tests/tests/common/vitrage_utils.py @@ -15,11 +15,12 @@ from datetime import datetime from vitrage.datasources import NOVA_HOST_DATASOURCE from vitrage.datasources import NOVA_INSTANCE_DATASOURCE -from vitrage_tempest_tests.tests.api.event.base import DOWN -from vitrage_tempest_tests.tests.api.event.base import UP from vitrage_tempest_tests.tests.common import general_utils as g_utils from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients +DOWN = 'down' +UP = 'up' + def generate_fake_host_alarm(hostname, event_type, enabled=True): details = { diff --git a/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py b/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py index de94ff163..16729117f 100644 --- a/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py +++ b/vitrage_tempest_tests/tests/notifiers/test_mistral_notifier.py @@ -17,10 +17,7 @@ from testtools.matchers import HasLength from vitrage import os_clients from vitrage_tempest_tests.tests.api.event.base import BaseTestEvents -from vitrage_tempest_tests.tests.api.event.base import DOWN -from vitrage_tempest_tests.tests.api.event.base import UP from vitrage_tempest_tests.tests.common.tempest_clients import TempestClients -from vitrage_tempest_tests.tests.common import vitrage_utils from vitrage_tempest_tests.tests import utils from vitrage_tempest_tests.tests.utils import wait_for_status @@ -46,25 +43,34 @@ wf_for_tempest_test_1234: class TestMistralNotifier(BaseTestEvents): + TRIGGER_ALARM_1 = "notifiers.mistral.trigger.alarm.1" + TRIGGER_ALARM_2 = "notifiers.mistral.trigger.alarm.2" + @classmethod def setUpClass(cls): super(TestMistralNotifier, cls).setUpClass() cls.mistral_client = os_clients.mistral_client(cls.conf) @utils.tempest_logger - def test_execute_mistral(self): - hostname = vitrage_utils.get_first_host()['name'] + def test_execute_mistral_v1(self): + self._do_test_execute_mistral(self.TRIGGER_ALARM_1) + @utils.tempest_logger + def test_execute_mistral_v2(self): + self._do_test_execute_mistral(self.TRIGGER_ALARM_2) + + def _do_test_execute_mistral(self, trigger_alarm): workflows = self.mistral_client.workflows.list() - self.assertIsNotNone(workflows) + self.assertIsNotNone(workflows, 'Failed to get the list of workflows') num_workflows = len(workflows) executions = self.mistral_client.executions.list() - self.assertIsNotNone(executions) + self.assertIsNotNone(executions, + 'Failed to get the list of workflow executions') num_executions = len(executions) alarms = utils.wait_for_answer(2, 0.5, self._check_alarms) - self.assertIsNotNone(alarms) + self.assertIsNotNone(alarms, 'Failed to get the list of alarms') num_alarms = len(alarms) try: @@ -73,54 +79,58 @@ class TestMistralNotifier(BaseTestEvents): # Validate the workflow creation workflows = self.mistral_client.workflows.list() - self.assertIsNotNone(workflows) - self.assertThat(workflows, HasLength(num_workflows + 1)) + self.assertIsNotNone(workflows, + 'Failed to get the list of workflows') + self.assertThat(workflows, HasLength(num_workflows + 1), + 'Mistral workflow was not created') - # Send a Doctor event that should generate an alarm. According to - # execute_mistral.yaml template, the alarm should cause execution - # of the workflow - details = self._create_doctor_event_details(hostname, DOWN) - self._post_event(details) + # Trigger an alarm. According to v1_execute_mistral.yaml template, + # the alarm should cause execution of the workflow + self._trigger_do_action(trigger_alarm) # Wait for the alarm to be raised self.assertTrue(wait_for_status( 10, self._check_num_vitrage_alarms, - num_alarms=num_alarms + 1)) + num_alarms=num_alarms + 1), + 'Trigger alarm was not raised') # Wait for the Mistral workflow execution self.assertTrue(wait_for_status( 20, self._check_mistral_workflow_execution, - num_executions=num_executions + 1)) + num_executions=num_executions + 1), + 'Mistral workflow was not executed') except Exception as e: self._handle_exception(e) raise finally: self._rollback_to_default(WF_NAME, num_workflows, - hostname, num_alarms) + trigger_alarm, num_alarms) pass def _rollback_to_default(self, workflow_name, num_workflows, - hostname, num_alarms): + trigger_alarm, num_alarms): # Delete the workflow self.mistral_client.workflows.delete(workflow_name) workflows = self.mistral_client.workflows.list() - self.assertIsNotNone(workflows) - self.assertThat(workflows, HasLength(num_workflows)) + self.assertIsNotNone(workflows, 'Failed to get the list of workflows') + self.assertThat(workflows, HasLength(num_workflows), + 'Failed to remove the test workflow') - # Clear the host down event and wait for the alarm to be deleted - details = self._create_doctor_event_details(hostname, UP) - self._post_event(details) + # Clear the trigger alarm and wait it to be deleted + self._trigger_undo_action(trigger_alarm) self.assertTrue(wait_for_status( 10, self._check_num_vitrage_alarms, - num_alarms=num_alarms)) + num_alarms=num_alarms), + 'Vitrage trigger alarm was not deleted') - def _check_num_vitrage_alarms(self, num_alarms): + @staticmethod + def _check_num_vitrage_alarms(num_alarms): vitrage_alarms = TempestClients.vitrage().alarm.list(vitrage_id='all', all_tenants=True) if len(vitrage_alarms) == num_alarms: diff --git a/vitrage_tempest_tests/tests/resources/templates/api/v1_execute_mistral.yaml b/vitrage_tempest_tests/tests/resources/templates/api/v1_execute_mistral.yaml new file mode 100644 index 000000000..94c1c645a --- /dev/null +++ b/vitrage_tempest_tests/tests/resources/templates/api/v1_execute_mistral.yaml @@ -0,0 +1,29 @@ +metadata: + version: 1 + name: v1_execute_mistral + description: execute mistral +definitions: + entities: + - entity: + category: ALARM + name: notifiers.mistral.trigger.alarm.1 + 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 + farewell: Hello and Goodbye 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 new file mode 100644 index 000000000..60585c334 --- /dev/null +++ b/vitrage_tempest_tests/tests/resources/templates/api/v2_execute_mistral.yaml @@ -0,0 +1,30 @@ +metadata: + version: 2 + name: v2_execute_mistral + description: execute mistral +definitions: + entities: + - entity: + category: ALARM + name: notifiers.mistral.trigger.alarm.2 + 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: Hello and Goodbye