vitrage evaluator - template validator

Change-Id: Iec95ee888dc4822f9bd478ce2434a89d1b00a2e1
This commit is contained in:
Liat Har-Tal 2016-02-04 13:07:33 +00:00
parent 641eaf6da2
commit df74a75090
9 changed files with 534 additions and 40 deletions

View File

@ -19,3 +19,4 @@ Werkzeug>=0.7
keystonemiddleware>=2.3.0 keystonemiddleware>=2.3.0
stevedore>=1.5.0 # Apache-2.0 stevedore>=1.5.0 # Apache-2.0
exrex>=0.9.4 exrex>=0.9.4
voluptuous>=0.8.8

View File

@ -22,3 +22,4 @@ testscenarios>=0.4
testtools>=1.4.0 testtools>=1.4.0
exrex>=0.9.4 exrex>=0.9.4
stevedore>=1.5.0 # Apache-2.0 stevedore>=1.5.0 # Apache-2.0
voluptuous>=0.8.8

View File

@ -12,9 +12,13 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os import os
import yaml import yaml
from oslo_log import log
LOG = log.getLogger(__name__)
def load_files(dir_path, suffix=None): def load_files(dir_path, suffix=None):
loaded_files = os.listdir(dir_path) loaded_files = os.listdir(dir_path)
@ -25,15 +29,21 @@ def load_files(dir_path, suffix=None):
return loaded_files return loaded_files
def load_yaml_files(dir_path): def load_yaml_files(dir_path, with_exception=False):
files = load_files(dir_path, '.yaml') files = load_files(dir_path, '.yaml')
yaml_files = [] yaml_files = []
for file in files: for file in files:
full_path = dir_path + '/' + file full_path = dir_path + '/' + file
with open(full_path, 'r') as stream: with open(full_path, 'r') as stream:
# TODO(alexey): check what to do if parse of one of the files fails try:
config = yaml.load(stream) 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) yaml_files.append(config)
return yaml_files return yaml_files

View File

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

View File

@ -11,27 +11,17 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import yaml
from oslo_log import log from oslo_log import log
from vitrage.common import file_utils from vitrage.common import file_utils
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
def load_templates_files(conf): def load_templates_files(conf):
templates_dir_path = conf.evaluator.templates_dir 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 template_files:
for template_file in templates_files: pass
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

View File

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

View File

@ -1,36 +1,60 @@
metadata: metadata:
id=host_high_cpu_load_to_instance_cpu_suboptimal id: host_high_cpu_load_to_instance_cpu_suboptimal
definitions: definitions:
entities: entities:
- entity: - entity:
category: ALARM category: ALARM
type: HOST_HIGH_CPU_LOAD type: HOST_HIGH_CPU_LOAD
internal_id: 1 template_id: 1
- entity: - entity:
category: ALARM category: ALARM
type: VM_CPU_SUBOPTIMAL_PERFORMANCE type: VM_CPU_SUBOPTIMAL_PERFORMANCE
internal_id: 2 template_id: 2
- entity: - entity:
category: RESOURCE category: RESOURCE
type: HOST type: HOST
internal_id: 3 template_id: 3
- entity: - entity:
category: RESOURCE category: RESOURCE
type: INSTANCE type: INSTANCE
internal_id: 4 template_id: 4
relationships: relationships:
- relationship: - relationship:
source: 1 source: 1
target: 3 target: 3
type: on relationship_type: on
internal_id : alarm_on_host template_id : alarm_on_host
- relationship: - relationship:
source: 2 source: 2
target: 4 target: 4
type: on relationship_type: on
internal_id : alarm_on_instance template_id : alarm_on_instance
- relationship: - relationship:
source: 3 source: 3
target: 4 target: 4
type: contains relationship_type: contains
internal_id : host_contains_instance 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

View File

@ -11,12 +11,10 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import os
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from vitrage.common import file_utils
from vitrage.evaluator import template_loader
from vitrage.tests import base from vitrage.tests import base
from vitrage.tests.mocks import utils from vitrage.tests.mocks import utils
@ -40,13 +38,9 @@ class TemplateLoaderTest(base.BaseTest):
self.conf = cfg.ConfigOpts() self.conf = cfg.ConfigOpts()
self.conf.register_opts(self.OPTS, group='evaluator') 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): def test_template_loader(self):
pass
# 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))

View File

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