From df74a7509029645682927f67969cb8351deb75d1 Mon Sep 17 00:00:00 2001 From: Liat Har-Tal Date: Thu, 4 Feb 2016 13:07:33 +0000 Subject: [PATCH] vitrage evaluator - template validator Change-Id: Iec95ee888dc4822f9bd478ce2434a89d1b00a2e1 --- requirements.txt | 1 + test-requirements.txt | 1 + vitrage/common/file_utils.py | 18 +- vitrage/evaluator/template_fields.py | 43 +++ vitrage/evaluator/template_loader.py | 16 +- vitrage/evaluator/template_validator.py | 278 ++++++++++++++++++ ...st_high_cpu_load_to_vm_cpu_suboptimal.yaml | 46 ++- .../unit/evaluator/test_template_loader.py | 18 +- .../unit/evaluator/test_template_validator.py | 153 ++++++++++ 9 files changed, 534 insertions(+), 40 deletions(-) create mode 100644 vitrage/evaluator/template_fields.py create mode 100644 vitrage/evaluator/template_validator.py create mode 100644 vitrage/tests/unit/evaluator/test_template_validator.py diff --git a/requirements.txt b/requirements.txt index 64cf4be75..05ced3b35 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,3 +19,4 @@ Werkzeug>=0.7 keystonemiddleware>=2.3.0 stevedore>=1.5.0 # Apache-2.0 exrex>=0.9.4 +voluptuous>=0.8.8 diff --git a/test-requirements.txt b/test-requirements.txt index 5b1506406..288e5f4a3 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -22,3 +22,4 @@ testscenarios>=0.4 testtools>=1.4.0 exrex>=0.9.4 stevedore>=1.5.0 # Apache-2.0 +voluptuous>=0.8.8 diff --git a/vitrage/common/file_utils.py b/vitrage/common/file_utils.py index 42921946d..dddcb7bd5 100644 --- a/vitrage/common/file_utils.py +++ b/vitrage/common/file_utils.py @@ -12,9 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. import os - import yaml +from oslo_log import log + + +LOG = log.getLogger(__name__) + def load_files(dir_path, suffix=None): loaded_files = os.listdir(dir_path) @@ -25,15 +29,21 @@ def load_files(dir_path, suffix=None): return loaded_files -def load_yaml_files(dir_path): +def load_yaml_files(dir_path, with_exception=False): files = load_files(dir_path, '.yaml') yaml_files = [] for file in files: full_path = dir_path + '/' + file with open(full_path, 'r') as stream: - # TODO(alexey): check what to do if parse of one of the files fails - config = yaml.load(stream) + try: + config = yaml.load(stream, Loader=yaml.BaseLoader) + except Exception as e: + if with_exception: + raise e + else: + LOG.error('Fails to parse file: %s. %s' % full_path, e) + yaml_files.append(config) return yaml_files diff --git a/vitrage/evaluator/template_fields.py b/vitrage/evaluator/template_fields.py new file mode 100644 index 000000000..481e5fefe --- /dev/null +++ b/vitrage/evaluator/template_fields.py @@ -0,0 +1,43 @@ +# 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. + + +class TemplateFields(object): + + METADATA = 'metadata' + DEFINITIONS = 'definitions' + SCENARIOS = 'scenarios' + + ENTITIES = 'entities' + ENTITY = 'entity' + CATEGORY = 'category' + + RELATIONSHIPS = 'relationships' + RELATIONSHIP = 'relationship' + RELATIONSHIP_TYPE = 'relationship_type' + + SCENARIO = 'scenario' + CONDITION = 'condition' + ACTIONS = 'actions' + ACTION = 'action' + ACTION_TYPE = 'action_type' + PROPERTIES = 'properties' + ACTION_TARGET = 'action_target' + + TEMPLATE_ID = 'template_id' + SOURCE = 'source' + TARGET = 'target' + TYPE = 'type' + + ID = 'id' diff --git a/vitrage/evaluator/template_loader.py b/vitrage/evaluator/template_loader.py index 2365c49c2..5d35a3f77 100644 --- a/vitrage/evaluator/template_loader.py +++ b/vitrage/evaluator/template_loader.py @@ -11,27 +11,17 @@ # 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 yaml - from oslo_log import log from vitrage.common import file_utils - LOG = log.getLogger(__name__) def load_templates_files(conf): templates_dir_path = conf.evaluator.templates_dir - templates_files = file_utils.load_files(templates_dir_path, '.yaml') + template_files = file_utils.load_yaml_files(templates_dir_path) - templates_configs = [] - for template_file in templates_files: - - full_path = templates_dir_path + '/' + template_file - with open(full_path, 'r') as stream: - config = yaml.load(stream) - templates_configs.append(config) - - return templates_configs + for template_file in template_files: + pass diff --git a/vitrage/evaluator/template_validator.py b/vitrage/evaluator/template_validator.py new file mode 100644 index 000000000..a3c55f11a --- /dev/null +++ b/vitrage/evaluator/template_validator.py @@ -0,0 +1,278 @@ +# Copyright 2015 - 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 voluptuous import Any +from voluptuous import Error +from voluptuous import Required +from voluptuous import Schema + +from vitrage.evaluator.template_fields import TemplateFields + + +LOG = log.getLogger(__name__) + + +MANDATORY_SECTIONS_ERROR = '"definitions", "metadata" and "scenarios are ' \ + 'mandatory sections in template file.' +TEMPLATE_VALIDATION_ERROR = 'Template validation failure.' +ELEMENTS_MIN_NUM_ERROR = 'At least one %s must be defined.' +DICT_STRUCTURE_SCHEMA_ERROR = '%s must refer to dictionary.' +SCHEMA_CONTENT_ERROR = '%s must contain %s Fields.' + + +def validate(template_conf): + + is_valid = validate_template_sections(template_conf) + + if is_valid: + is_metadata_valid = validate_metadata_section( + template_conf[TemplateFields.METADATA]) + is_defs_valid = validate_definitions_section( + template_conf[TemplateFields.DEFINITIONS]) + is_scenarios_valid = validate_scenarios_section( + template_conf[TemplateFields.SCENARIOS]) + + return is_metadata_valid and is_defs_valid and is_scenarios_valid + + return False + + +def validate_template_sections(template_conf): + + schema = Schema({ + Required(TemplateFields.DEFINITIONS): dict, + Required(TemplateFields.METADATA): dict, + Required(TemplateFields.SCENARIOS): list + }) + return _validate_dict_schema( + schema, template_conf, MANDATORY_SECTIONS_ERROR) + + +def validate_metadata_section(metadata): + + schema = Schema({ + Required(TemplateFields.ID): Any(str, basestring) + }) + + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.METADATA, TemplateFields.ID) + return _validate_dict_schema(schema, metadata, error_msg) + + +def validate_definitions_section(definitions): + + schema = Schema({ + Required(TemplateFields.ENTITIES): list, + TemplateFields.RELATIONSHIPS: list + }) + + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.DEFINITIONS, + '"%s"' % TemplateFields.ENTITIES + ) + is_defs_valid = _validate_dict_schema(schema, definitions, error_msg) + + if is_defs_valid: + is_entities_valid = validate_entities( + definitions[TemplateFields.ENTITIES] + ) + + relationships = definitions.get(TemplateFields.RELATIONSHIPS, None) + is_relationships_valid = True + if relationships: + is_relationships_valid = validate_relationships(relationships) + + return is_relationships_valid and is_entities_valid + + return False + + +def validate_entities(entities): + + if len(entities) <= 0: + error_msg = ELEMENTS_MIN_NUM_ERROR % TemplateFields.ENTITY + LOG.error(_build_error_message(error_msg)) + return False + + for entity in entities: + + try: + Schema({ + Required(TemplateFields.ENTITY): dict, + })(entity) + except Error as e: + error_msg = DICT_STRUCTURE_SCHEMA_ERROR % TemplateFields.ENTITY + LOG.error(_build_error_message(error_msg, e)) + return False + + return validate_entity(entity[TemplateFields.ENTITY]) + + +def validate_entity(entity): + + schema = Schema({ + Required(TemplateFields.CATEGORY): Any(str, basestring), + TemplateFields.TYPE: Any(str, basestring), + Required(TemplateFields.TEMPLATE_ID): Any(str, basestring, int) + }) + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.ENTITY, + '"%s" and "%s"' % (TemplateFields.CATEGORY, TemplateFields.TEMPLATE_ID) + ) + return _validate_dict_schema(schema, entity, error_msg) + + +def validate_relationships(relationships): + + for relationship in relationships: + + try: + Schema({ + Required(TemplateFields.RELATIONSHIP): dict, + })(relationship) + except Error as e: + error_msg = DICT_STRUCTURE_SCHEMA_ERROR % ( + TemplateFields.RELATIONSHIP + ) + LOG.error(_build_error_message(error_msg, e)) + return False + + return validate_relationship(relationship[TemplateFields.RELATIONSHIP]) + + +def validate_relationship(relationship): + + schema = Schema({ + Required(TemplateFields.SOURCE): Any(str, basestring, int), + Required(TemplateFields.TARGET): Any(str, basestring, int), + TemplateFields.RELATIONSHIP_TYPE: Any(str, basestring), + Required(TemplateFields.TEMPLATE_ID): Any(str, basestring, int) + }) + + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.RELATIONSHIP, '"%s", "%s" and "%s"' % ( + TemplateFields.SOURCE, + TemplateFields.TARGET, + TemplateFields.RELATIONSHIP_TYPE + ) + ) + return _validate_dict_schema(schema, relationship, error_msg) + + +def validate_scenarios_section(scenarios): + + if len(scenarios) <= 0: + error_msg = ELEMENTS_MIN_NUM_ERROR % TemplateFields.SCENARIOS + LOG.error(_build_error_message(error_msg)) + return False + + for scenario in scenarios: + + try: + Schema({ + Required(TemplateFields.SCENARIO): dict, + })(scenario) + except Error as e: + error_msg = DICT_STRUCTURE_SCHEMA_ERROR % TemplateFields.SCENARIO + LOG.error(_build_error_message(error_msg, e)) + return False + + is_valid = validate_scenario(scenario[TemplateFields.SCENARIO]) + if not is_valid: + return False + + return True + + +def validate_scenario(scenario): + + schema = Schema({ + Required(TemplateFields.CONDITION): Any(str, basestring), + Required(TemplateFields.ACTIONS): list + }) + + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.SCENARIOS, + '"%s" and "%s"' % (TemplateFields.CONDITION, TemplateFields.ACTIONS) + ) + is_scenario_valid = _validate_dict_schema( + schema, scenario, error_msg) + + if is_scenario_valid: + return validate_actions_schema(scenario[TemplateFields.ACTIONS]) + + return False + + +def validate_actions_schema(actions): + + if len(actions) <= 0: + error_message = ELEMENTS_MIN_NUM_ERROR % TemplateFields.ACTION + LOG.error(_build_error_message(error_message)) + return False + + for action in actions: + + try: + Schema({ + Required(TemplateFields.ACTION): dict, + })(action) + except Error as e: + msg = DICT_STRUCTURE_SCHEMA_ERROR % TemplateFields.ACTION + LOG.error(_build_error_message(msg, e)) + return False + + is_action_valid = validate_action_schema(action[TemplateFields.ACTION]) + if not is_action_valid: + return False + + return True + + +def validate_action_schema(action): + + schema = Schema({ + Required(TemplateFields.ACTION_TYPE): Any(str, basestring), + TemplateFields.PROPERTIES: dict, + Required(TemplateFields.ACTION_TARGET): dict + }) + + error_msg = SCHEMA_CONTENT_ERROR % ( + TemplateFields.ACTION, + '"%s" and "%s"' % ( + TemplateFields.ACTION_TYPE, + TemplateFields.ACTION_TARGET + ) + ) + return _validate_dict_schema(schema, action, error_msg) + + +def _build_error_message(message, e=None): + + if e: + return '%s %s %s' % (TEMPLATE_VALIDATION_ERROR, message, e) + else: + return '%s %s' % (TEMPLATE_VALIDATION_ERROR, message) + + +def _validate_dict_schema(schema, value, error_message): + + try: + schema(value) + except Error as e: + LOG.error(_build_error_message(error_message, e)) + return False + + return True diff --git a/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml b/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml index 04c6d1155..23dc1f851 100644 --- a/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml +++ b/vitrage/tests/resources/templates/host_high_cpu_load_to_vm_cpu_suboptimal.yaml @@ -1,36 +1,60 @@ metadata: - id=host_high_cpu_load_to_instance_cpu_suboptimal + id: host_high_cpu_load_to_instance_cpu_suboptimal definitions: entities: - entity: category: ALARM type: HOST_HIGH_CPU_LOAD - internal_id: 1 + template_id: 1 - entity: category: ALARM type: VM_CPU_SUBOPTIMAL_PERFORMANCE - internal_id: 2 + template_id: 2 - entity: category: RESOURCE type: HOST - internal_id: 3 + template_id: 3 - entity: category: RESOURCE type: INSTANCE - internal_id: 4 + template_id: 4 relationships: - relationship: source: 1 target: 3 - type: on - internal_id : alarm_on_host + relationship_type: on + template_id : alarm_on_host - relationship: source: 2 target: 4 - type: on - internal_id : alarm_on_instance + relationship_type: on + template_id : alarm_on_instance - relationship: source: 3 target: 4 - type: contains - internal_id : host_contains_instance \ No newline at end of file + relationship_type: contains + template_id : host_contains_instance +scenarios: + - scenario: + condition: alarm_on_host and host_contains_instance + actions: + - action: + action_type: raise_alarm + properties: + alarm_type: VM_CPU_SUBOPTIMAL_PERFORMANCE + action_target: + target: 4 + - action: + action_type: set_state + properties: + state: SUBOPTIMAL + action_target: + target: 4 + - scenario: + condition: alarm_on_host and alarm_on_instance and host_contains_instance + actions: + - action: + action_type: add_causal_relationship + action_target: + source: 1 + target: 2 diff --git a/vitrage/tests/unit/evaluator/test_template_loader.py b/vitrage/tests/unit/evaluator/test_template_loader.py index 4c53503d8..dd7c46d0c 100644 --- a/vitrage/tests/unit/evaluator/test_template_loader.py +++ b/vitrage/tests/unit/evaluator/test_template_loader.py @@ -11,12 +11,10 @@ # 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 os from oslo_config import cfg from oslo_log import log as logging - -from vitrage.evaluator import template_loader +from vitrage.common import file_utils from vitrage.tests import base from vitrage.tests.mocks import utils @@ -40,13 +38,9 @@ class TemplateLoaderTest(base.BaseTest): self.conf = cfg.ConfigOpts() self.conf.register_opts(self.OPTS, group='evaluator') + self.template_yamls = file_utils.load_yaml_files( + self.template_dir_path + ) + def test_template_loader(self): - - # Setup - total_templates = os.listdir(self.template_dir_path) - - # Action - template_configs = template_loader.load_templates_files(self.conf) - - # Test assertions - self.assertEqual(len(total_templates), len(template_configs)) + pass diff --git a/vitrage/tests/unit/evaluator/test_template_validator.py b/vitrage/tests/unit/evaluator/test_template_validator.py new file mode 100644 index 000000000..e3cd17aa4 --- /dev/null +++ b/vitrage/tests/unit/evaluator/test_template_validator.py @@ -0,0 +1,153 @@ +# 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 copy + +from oslo_log import log as logging + +from vitrage.common import file_utils +from vitrage.evaluator.template_fields import TemplateFields +from vitrage.evaluator import template_validator +from vitrage.tests import base +from vitrage.tests.mocks import utils + + +LOG = logging.getLogger(__name__) + + +# noinspection PyAttributeOutsideInit +class TemplateValidatorTest(base.BaseTest): + + @classmethod + def setUpClass(cls): + + template_dir_path = '%s/templates' % utils.get_resources_dir() + template_yamls = file_utils.load_yaml_files(template_dir_path) + cls.first_template = template_yamls[0] + + @property + def clone_template(self): + return copy.deepcopy(self.first_template) + + def test_template_validator(self): + self.assertTrue(template_validator.validate(self.first_template)) + + def test_validate_template_without_metadata_section(self): + + template = self.clone_template + template.pop(TemplateFields.METADATA) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_without_id_in_metadata_section(self): + + template = self.clone_template + template[TemplateFields.METADATA].pop(TemplateFields.ID) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_without_definitions_section(self): + + template = self.clone_template + template.pop(TemplateFields.DEFINITIONS) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_without_entities(self): + + template = self.clone_template + template[TemplateFields.DEFINITIONS].pop(TemplateFields.ENTITIES) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_with_empty_entities(self): + + template = self.clone_template + template[TemplateFields.DEFINITIONS][TemplateFields.ENTITIES] = [] + self.assertFalse(template_validator.validate(template)) + + def test_validate_entity_without_required_fields(self): + + template = self.clone_template + definitions = template[TemplateFields.DEFINITIONS] + entity = definitions[TemplateFields.ENTITIES][0] + entity[TemplateFields.ENTITY].pop(TemplateFields.CATEGORY) + self.assertFalse(template_validator.validate(template)) + + template = self.clone_template + definitions = template[TemplateFields.DEFINITIONS] + entity = definitions[TemplateFields.ENTITIES][0] + entity[TemplateFields.ENTITY].pop(TemplateFields.TEMPLATE_ID) + self.assertFalse(template_validator.validate(template)) + + def test_validate_relationships_without_required_fields(self): + + template = self.clone_template + definitions = template[TemplateFields.DEFINITIONS] + relationship = definitions[TemplateFields.RELATIONSHIPS][0] + relationship[TemplateFields.RELATIONSHIP].pop(TemplateFields.SOURCE) + self.assertFalse(template_validator.validate(template)) + + template = self.clone_template + definitions = template[TemplateFields.DEFINITIONS] + relationship = definitions[TemplateFields.RELATIONSHIPS][0] + relationship[TemplateFields.RELATIONSHIP].pop(TemplateFields.TARGET) + self.assertFalse(template_validator.validate(template)) + + template = self.clone_template + definitions = template[TemplateFields.DEFINITIONS] + relationship = definitions[TemplateFields.RELATIONSHIPS][0] + relationship[TemplateFields.RELATIONSHIP].pop( + TemplateFields.TEMPLATE_ID + ) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_without_scenarios(self): + + template = self.clone_template + template.pop(TemplateFields.SCENARIOS) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_with_empty_scenarios(self): + template = self.clone_template + template[TemplateFields.SCENARIOS] = [] + self.assertFalse(template_validator.validate(template)) + + def test_validate_scenario_without_required_fields(self): + + template = self.clone_template + scenario = template[TemplateFields.SCENARIOS][0] + scenario[TemplateFields.SCENARIO].pop(TemplateFields.CONDITION) + self.assertFalse(template_validator.validate(template)) + + template = self.clone_template + scenario = template[TemplateFields.SCENARIOS][0] + scenario[TemplateFields.SCENARIO].pop(TemplateFields.ACTIONS) + self.assertFalse(template_validator.validate(template)) + + def test_validate_template_with_empty_actions(self): + + template = self.clone_template + scenario = template[TemplateFields.SCENARIOS][0] + scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS] = [] + self.assertFalse(template_validator.validate(template)) + + def test_validate_action_without_required_fields(self): + + template = self.clone_template + scenario = template[TemplateFields.SCENARIOS][0] + action = scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS][0] + action[TemplateFields.ACTION].pop(TemplateFields.ACTION_TYPE) + self.assertFalse(template_validator.validate(template)) + + template = self.clone_template + scenario = template[TemplateFields.SCENARIOS][0] + action = scenario[TemplateFields.SCENARIO][TemplateFields.ACTIONS][0] + action[TemplateFields.ACTION].pop(TemplateFields.ACTION_TARGET) + self.assertFalse(template_validator.validate(template))