diff --git a/etc/vitrage/datasources_values/doctor.yaml b/etc/vitrage/datasources_values/doctor.yaml new file mode 100644 index 000000000..6419f49ca --- /dev/null +++ b/etc/vitrage/datasources_values/doctor.yaml @@ -0,0 +1,12 @@ +category: ALARM +values: + - aggregated values: + priority: 40 + original values: + - name: critical + operational_value: CRITICAL + - aggregated values: + priority: 10 + original values: + - name: OK + operational_value: OK diff --git a/etc/vitrage/templates.sample/host_down_scenarios.yaml b/etc/vitrage/templates.sample/host_down_scenarios.yaml new file mode 100644 index 000000000..746bd1648 --- /dev/null +++ b/etc/vitrage/templates.sample/host_down_scenarios.yaml @@ -0,0 +1,70 @@ +metadata: + name: host_down_scenarios + description: scenarios triggered by Doctor monitor 'compute.host.down' alarm +definitions: + entities: + - entity: + category: ALARM + name: compute.host.down + template_id: host_down_alarm + - entity: + category: ALARM + type: vitrage + name: Instance Down + template_id: instance_alarm + - entity: + category: RESOURCE + type: nova.instance + template_id: instance + - entity: + category: RESOURCE + type: nova.host + template_id: host + relationships: + - relationship: + source: host_down_alarm + relationship_type: on + target: host + template_id : host_down_alarm_on_host + - relationship: + source: host + relationship_type: contains + target: instance + template_id : host_contains_instance + - relationship: + source: instance_alarm + relationship_type: on + target: instance + template_id : alarm_on_instance +scenarios: + - scenario: + condition: host_down_alarm_on_host + actions: + - action: + action_type: set_state + action_target: + target: host + properties: + state: ERROR + - action: + action_type: mark_down + action_target: + target: host + - scenario: + condition: host_down_alarm_on_host and host_contains_instance + actions: + - action: + action_type: raise_alarm + action_target: + target: instance + properties: + alarm_name: Instance Down + severity: critical + - scenario: + condition: host_down_alarm_on_host and host_contains_instance and alarm_on_instance + actions: + - action: + action_type: add_causal_relationship + action_target: + source: host_down_alarm + target: instance_alarm diff --git a/vitrage/datasources/alarm_driver_base.py b/vitrage/datasources/alarm_driver_base.py index c1209dc33..ed0bf0cf0 100644 --- a/vitrage/datasources/alarm_driver_base.py +++ b/vitrage/datasources/alarm_driver_base.py @@ -141,3 +141,7 @@ class AlarmDriverBase(DriverBase): ret = alarm if filter_(alarm, old_alarm) else None self.cache[self._alarm_key(alarm)] = alarm, time return ret + + def _old_alarm(self, event): + alarm_key = self._alarm_key(event) + return self.cache.get(alarm_key, (None, None))[0] diff --git a/vitrage/datasources/alarm_transformer_base.py b/vitrage/datasources/alarm_transformer_base.py index 73d32b3de..00b5b649a 100644 --- a/vitrage/datasources/alarm_transformer_base.py +++ b/vitrage/datasources/alarm_transformer_base.py @@ -16,11 +16,16 @@ from oslo_log import log as logging from vitrage.common.constants import DatasourceAction from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EdgeLabel from vitrage.common.constants import EntityCategory from vitrage.common.constants import GraphAction +from vitrage.common.constants import VertexProperties as VProps from vitrage.common.exception import VitrageTransformerError from vitrage.datasources.alarm_properties import AlarmProperties as AlarmProps +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE from vitrage.datasources import transformer_base as tbase +from vitrage.datasources.transformer_base import Neighbor +import vitrage.graph.utils as graph_utils LOG = logging.getLogger(__name__) @@ -63,11 +68,36 @@ class AlarmTransformerBase(tbase.TransformerBase): def _get_alarm_state(self, entity_event): event_type = entity_event.get(DSProps.EVENT_TYPE, None) - if event_type is not None: - return AlarmProps.INACTIVE_STATE if \ - GraphAction.DELETE_ENTITY == event_type else \ - AlarmProps.ACTIVE_STATE + if event_type and event_type == GraphAction.DELETE_ENTITY: + return AlarmProps.INACTIVE_STATE else: return AlarmProps.INACTIVE_STATE if \ self._ok_status(entity_event) else \ AlarmProps.ACTIVE_STATE + + def _create_neighbor(self, + vitrage_id, + sample_timestamp, + resource_type, + resource_name): + # Any resource transformer will do (nova for example) + transformer = self.transformers[NOVA_HOST_DATASOURCE] + + if transformer: + properties = { + VProps.TYPE: resource_type, + VProps.ID: resource_name, + VProps.SAMPLE_TIMESTAMP: sample_timestamp + } + resource_vertex = transformer.create_placeholder_vertex( + **properties) + + relationship_edge = graph_utils.create_edge( + source_id=vitrage_id, + target_id=resource_vertex.vertex_id, + relationship_type=EdgeLabel.ON) + + return Neighbor(resource_vertex, relationship_edge) + + LOG.warning('Cannot create neighbour, host transformer does not exist') + return None diff --git a/vitrage/datasources/aodh/driver.py b/vitrage/datasources/aodh/driver.py index 75e33a98b..5ee388d66 100644 --- a/vitrage/datasources/aodh/driver.py +++ b/vitrage/datasources/aodh/driver.py @@ -254,8 +254,7 @@ class AodhDriver(AlarmDriverBase): "event_type": "instance.update"}} """ - alarm_key = self._alarm_key(event) - old_alarm = self.cache.get(alarm_key, (None, None))[0] + old_alarm = self._old_alarm(event) entity = old_alarm.copy() changed_rule = event[AodhProps.DETAIL] @@ -273,8 +272,7 @@ class AodhDriver(AlarmDriverBase): datetime_utils.utcnow(False)) def _convert_alarm_state_transition_event(self, event): - alarm_key = self._alarm_key(event) - old_alarm = self.cache.get(alarm_key, (None, None))[0] + old_alarm = self._old_alarm(event) entity = old_alarm.copy() entity[AodhProps.STATE] = event[AodhProps.DETAIL][AodhProps.STATE] diff --git a/vitrage/datasources/doctor/__init__.py b/vitrage/datasources/doctor/__init__.py new file mode 100644 index 000000000..b5c0d8dc8 --- /dev/null +++ b/vitrage/datasources/doctor/__init__.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. + +from oslo_config import cfg +from vitrage.common.constants import UpdateMethod + +DOCTOR_DATASOURCE = 'doctor' + +OPTS = [ + cfg.StrOpt('transformer', + default='vitrage.datasources.doctor.transformer.' + 'DoctorTransformer', + help='Doctor transformer class path', + required=True), + cfg.StrOpt('driver', + default='vitrage.datasources.doctor.driver.DoctorDriver', + help='Doctor driver class path', + required=True), + cfg.StrOpt('update_method', + default=UpdateMethod.PUSH, + help='None: updates only via Vitrage periodic snapshots.' + 'Pull: updates every [changes_interval] seconds.' + 'Push: updates by getting notifications from the' + ' datasource itself.', + required=True), +] diff --git a/vitrage/datasources/doctor/driver.py b/vitrage/datasources/doctor/driver.py new file mode 100644 index 000000000..5996a6fd6 --- /dev/null +++ b/vitrage/datasources/doctor/driver.py @@ -0,0 +1,112 @@ +# 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 collections import namedtuple +from oslo_log import log + +from vitrage.common.constants import DatasourceAction +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.datasources.alarm_driver_base import AlarmDriverBase +from vitrage.datasources.doctor import DOCTOR_DATASOURCE +from vitrage.datasources.doctor.properties import DoctorDetails +from vitrage.datasources.doctor.properties import DoctorProperties \ + as DoctorProps +from vitrage.datasources.doctor.properties import DoctorStatus +from vitrage.datasources.doctor.properties import get_detail + +LOG = log.getLogger(__name__) + + +class DoctorDriver(AlarmDriverBase): + AlarmKey = namedtuple('AlarmKey', ['alarm_name', 'hostname']) + + def __init__(self, conf): + super(DoctorDriver, self).__init__() + self.conf = conf + self._client = None + + def _entity_type(self): + return DOCTOR_DATASOURCE + + def _alarm_key(self, alarm): + return self.AlarmKey(alarm_name=alarm[DoctorProps.TYPE], + hostname=get_detail(alarm, + DoctorDetails.HOSTNAME)) + + def _is_erroneous(self, alarm): + return alarm and \ + get_detail(alarm, DoctorDetails.STATUS) != DoctorStatus.UP + + def _is_valid(self, alarm): + if not alarm or DoctorProps.TIME not in alarm or \ + DoctorProps.TYPE not in alarm or \ + DoctorProps.DETAILS not in alarm: + return False + + details = alarm[DoctorProps.DETAILS] + return DoctorDetails.STATUS in details and \ + DoctorDetails.SEVERITY in details and \ + DoctorDetails.HOSTNAME in details + + def _status_changed(self, new_alarm, old_alarm): + return get_detail(old_alarm, DoctorDetails.STATUS) != \ + get_detail(new_alarm, DoctorDetails.STATUS) + + def _get_alarms(self): + # pulling alarms is not supported in Doctor monitor + return [] + + def enrich_event(self, event, event_type): + """Enrich the given event + + :param event: dictionary of this form: + { + 'time': '2016-04-12T08:00:00.12345', + 'type': 'compute.host.down', + 'details': { + 'hostname': 'compute-1', + 'source': 'sample_monitor', + 'cause': 'link-down', + 'severity': 'critical', + 'status': 'down', + 'monitor_id': 'monitor-1', + 'monitor_event_id': '123', + } + } + :param event_type: always 'compute.host.down' + :return: the same event, with the following changes: + - DoctorProps.UPDATE_TIME - the event 'time' if it is new, or the + update time of the same event if it is already cached + + """ + + event[DSProps.EVENT_TYPE] = event_type + + old_alarm = self._old_alarm(event) + if old_alarm and not self._status_changed(old_alarm, event): + event[DoctorProps.UPDATE_TIME] = old_alarm[DoctorProps.UPDATE_TIME] + else: + event[DoctorProps.UPDATE_TIME] = event[DoctorProps.TIME] + + event = self._filter_and_cache_alarm(event, old_alarm, + self._filter_get_erroneous, + event[DoctorProps.TIME]) + + if event: + return DoctorDriver.make_pickleable([event], DOCTOR_DATASOURCE, + DatasourceAction.UPDATE)[0] + + @staticmethod + def get_event_types(): + return [DoctorProps.HOST_DOWN] diff --git a/vitrage/datasources/doctor/properties.py b/vitrage/datasources/doctor/properties.py new file mode 100644 index 000000000..582f218c8 --- /dev/null +++ b/vitrage/datasources/doctor/properties.py @@ -0,0 +1,42 @@ +# 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 DoctorProperties(object): + TIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%f' + HOST_DOWN = 'compute.host.down' + HOST_TYPE = 'nova.host' + TYPE = 'type' + TIME = 'time' + UPDATE_TIME = 'update_time' + DETAILS = 'details' + + +class DoctorDetails(object): + """The details appear inside a details section in the event """ + HOSTNAME = 'hostname' + STATUS = 'status' + SEVERITY = 'severity' + + +class DoctorStatus(object): + DOWN = 'down' + UP = 'up' + + +def get_detail(alarm, detail): + return alarm[DoctorProperties.DETAILS][detail] if \ + alarm and DoctorProperties.DETAILS in alarm and \ + detail in alarm[DoctorProperties.DETAILS] \ + else None diff --git a/vitrage/datasources/doctor/transformer.py b/vitrage/datasources/doctor/transformer.py new file mode 100644 index 000000000..863375910 --- /dev/null +++ b/vitrage/datasources/doctor/transformer.py @@ -0,0 +1,97 @@ +# 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 oslo_log import log as logging + +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.alarm_transformer_base import AlarmTransformerBase +from vitrage.datasources.doctor import DOCTOR_DATASOURCE +from vitrage.datasources.doctor.properties import DoctorDetails +from vitrage.datasources.doctor.properties import DoctorProperties \ + as DoctorProps +from vitrage.datasources.doctor.properties import DoctorStatus +from vitrage.datasources.doctor.properties import get_detail +from vitrage.datasources import transformer_base as tbase +import vitrage.graph.utils as graph_utils +from vitrage.utils.datetime import change_time_str_format + +LOG = logging.getLogger(__name__) + + +class DoctorTransformer(AlarmTransformerBase): + + def __init__(self, transformers, conf): + super(DoctorTransformer, self).__init__(transformers, conf) + + def _create_snapshot_entity_vertex(self, entity_event): + # The Doctor monitor does not support snapshot mode + return None + + def _create_update_entity_vertex(self, entity_event): + self._unify_time_format(entity_event) + + details = entity_event.get(DoctorProps.DETAILS, {}) + details[VProps.NAME] = entity_event[DoctorProps.TYPE] + + return graph_utils.create_vertex( + self._create_entity_key(entity_event), + entity_category=EntityCategory.ALARM, + entity_type=entity_event[DSProps.ENTITY_TYPE], + entity_state=self._get_alarm_state(entity_event), + sample_timestamp=entity_event[DoctorProps.TIME], + update_timestamp=entity_event[DoctorProps.UPDATE_TIME], + metadata=details) + + def _create_update_neighbors(self, entity_event): + return [self._create_neighbor( + self._create_entity_key(entity_event), + entity_event[DoctorProps.TIME], + DoctorProps.HOST_TYPE, + get_detail(entity_event, DoctorDetails.HOSTNAME))] + + def _create_entity_key(self, entity_event): + return tbase.build_key(self._key_values( + entity_event[DSProps.ENTITY_TYPE], + entity_event[DoctorProps.TYPE], + get_detail(entity_event, DoctorDetails.HOSTNAME))) + + def get_type(self): + return DOCTOR_DATASOURCE + + def _ok_status(self, entity_event): + return entity_event and \ + get_detail(entity_event, DoctorDetails.STATUS) == DoctorStatus.UP + + @staticmethod + def get_enrich_query(event): + hostname = get_detail(event, DoctorDetails.HOSTNAME) + if not hostname: + return None + return {VProps.ID: hostname} + + @staticmethod + def _unify_time_format(entity_event): + DoctorTransformer._unify_prop_time_format(entity_event, + DoctorProps.TIME) + DoctorTransformer._unify_prop_time_format(entity_event, + DoctorProps.UPDATE_TIME) + + @staticmethod + def _unify_prop_time_format(entity_event, prop): + entity_event[prop] = change_time_str_format( + entity_event[prop], + DoctorProps.TIME_FORMAT, + tbase.TIMESTAMP_FORMAT) diff --git a/vitrage/datasources/nagios/transformer.py b/vitrage/datasources/nagios/transformer.py index 8eba5076e..fea6b2213 100644 --- a/vitrage/datasources/nagios/transformer.py +++ b/vitrage/datasources/nagios/transformer.py @@ -15,16 +15,13 @@ from oslo_log import log as logging from vitrage.common.constants import DatasourceProperties as DSProps -from vitrage.common.constants import EdgeLabel from vitrage.common.constants import EntityCategory from vitrage.common.constants import VertexProperties as VProps from vitrage.datasources.alarm_transformer_base import AlarmTransformerBase from vitrage.datasources.nagios import NAGIOS_DATASOURCE from vitrage.datasources.nagios.properties import NagiosProperties from vitrage.datasources.nagios.properties import NagiosTestStatus -from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE from vitrage.datasources import transformer_base as tbase -from vitrage.datasources.transformer_base import Neighbor import vitrage.graph.utils as graph_utils from vitrage.utils import datetime as datetime_utils @@ -92,33 +89,6 @@ class NagiosTransformer(AlarmTransformerBase): return [] - def _create_neighbor(self, - vitrage_id, - sample_timestamp, - resource_type, - resource_name): - # Any resource transformer will do (nova for example) - transformer = self.transformers[NOVA_HOST_DATASOURCE] - - if transformer: - properties = { - VProps.TYPE: resource_type, - VProps.ID: resource_name, - VProps.SAMPLE_TIMESTAMP: sample_timestamp - } - resource_vertex = transformer.create_placeholder_vertex( - **properties) - - relationship_edge = graph_utils.create_edge( - source_id=vitrage_id, - target_id=resource_vertex.vertex_id, - relationship_type=EdgeLabel.ON) - - return Neighbor(resource_vertex, relationship_edge) - - LOG.warning('Cannot transform host, host transformer does not exist') - return None - def _ok_status(self, entity_event): return entity_event[NagiosProperties.STATUS] == NagiosTestStatus.OK diff --git a/vitrage/datasources/zabbix/transformer.py b/vitrage/datasources/zabbix/transformer.py index 25b541c31..27378ee89 100644 --- a/vitrage/datasources/zabbix/transformer.py +++ b/vitrage/datasources/zabbix/transformer.py @@ -15,14 +15,11 @@ from oslo_log import log as logging from vitrage.common.constants import DatasourceProperties as DSProps -from vitrage.common.constants import EdgeLabel from vitrage.common.constants import EntityCategory from vitrage.common.constants import VertexProperties as VProps from vitrage.datasources.alarm_properties import AlarmProperties as AlarmProps from vitrage.datasources.alarm_transformer_base import AlarmTransformerBase -from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE from vitrage.datasources import transformer_base as tbase -from vitrage.datasources.transformer_base import Neighbor from vitrage.datasources.zabbix.properties import ZabbixProperties as ZProps from vitrage.datasources.zabbix.properties import ZabbixTriggerSeverity \ as TriggerSeverity @@ -104,33 +101,6 @@ class ZabbixTransformer(AlarmTransformerBase): return [] - def _create_neighbor(self, - vitrage_id, - sample_timestamp, - resource_type, - resource_name): - # Any resource transformer will do (nova for example) - transformer = self.transformers[NOVA_HOST_DATASOURCE] - - if transformer: - properties = { - VProps.TYPE: resource_type, - VProps.ID: resource_name, - VProps.SAMPLE_TIMESTAMP: sample_timestamp - } - resource_vertex = transformer.create_placeholder_vertex( - **properties) - - relationship_edge = graph_utils.create_edge( - source_id=vitrage_id, - target_id=resource_vertex.vertex_id, - relationship_type=EdgeLabel.ON) - - return Neighbor(resource_vertex, relationship_edge) - - LOG.warning('Cannot transform host, host transformer does not exist') - return None - def _ok_status(self, entity_event): return entity_event[ZProps.VALUE] == TriggerValue.OK diff --git a/vitrage/entity_graph/transformer_manager.py b/vitrage/entity_graph/transformer_manager.py index ac0c05c2d..b2b29d6df 100644 --- a/vitrage/entity_graph/transformer_manager.py +++ b/vitrage/entity_graph/transformer_manager.py @@ -39,13 +39,19 @@ class TransformerManager(object): transformers = {} for datasource_type in conf.datasources.types: - transformers[datasource_type] = importutils.import_object( - conf[datasource_type].transformer, - transformers, conf) - if opt_exists(conf[datasource_type], ENTITIES): - for entity in conf[datasource_type].entities: - transformers[entity] = importutils.import_object( - conf[datasource_type].transformer, transformers, conf) + try: + transformers[datasource_type] = importutils.import_object( + conf[datasource_type].transformer, + transformers, conf) + if opt_exists(conf[datasource_type], ENTITIES): + for entity in conf[datasource_type].entities: + transformers[entity] = importutils.import_object( + conf[datasource_type].transformer, + transformers, conf) + + except Exception as e: + LOG.exception('Failed to register transformer %s. ' + 'Exception: %s', datasource_type, e) transformers[VITRAGE_TYPE] = importutils.import_object( "%s.%s" % (EvaluatorEventTransformer.__module__, diff --git a/vitrage/tests/mocks/mock_driver.py b/vitrage/tests/mocks/mock_driver.py index e6d0fccfe..8fc0f05a0 100644 --- a/vitrage/tests/mocks/mock_driver.py +++ b/vitrage/tests/mocks/mock_driver.py @@ -437,6 +437,27 @@ def simple_zabbix_alarm_generators(host_num, return tg.get_trace_generators(test_entity_spec_list) +def simple_doctor_alarm_generators(update_vals=None): + """A function for returning Doctor alarm event generators. + + Returns generators for a given number of Doctor alarms. + + :param update_vals: preset values for ALL update events + :return: generators for alarms as specified + """ + + test_entity_spec_list = [({ + tg.DYNAMIC_INFO_FKEY: tg.DRIVER_DOCTOR_UPDATE_D, + tg.STATIC_INFO_FKEY: None, + tg.EXTERNAL_INFO_KEY: update_vals, + tg.MAPPING_KEY: None, + tg.NAME_KEY: 'Doctor alarm generator', + tg.NUM_EVENTS: 1 + })] + + return tg.get_trace_generators(test_entity_spec_list) + + def simple_aodh_alarm_notification_generators(alarm_num, update_events=0, update_vals=None): diff --git a/vitrage/tests/mocks/mock_transformer.py b/vitrage/tests/mocks/mock_transformer.py index b13629cf3..ee1472db5 100644 --- a/vitrage/tests/mocks/mock_transformer.py +++ b/vitrage/tests/mocks/mock_transformer.py @@ -188,3 +188,25 @@ def simple_aodh_update_alarm_generators(alarm_num, } ] return tg.get_trace_generators(test_entity_spec_list) + + +def simple_doctor_alarm_generators(update_vals=None): + """A function for returning Doctor alarm event generators. + + Returns generators for a given number of Doctor alarms. + + :param update_vals: preset values for ALL update events + :return: generators for alarms as specified + """ + + test_entity_spec_list = [({ + tg.DYNAMIC_INFO_FKEY: tg.TRANS_DOCTOR_UPDATE_D, + tg.DYNAMIC_INFO_FPATH: tg.MOCK_TRANSFORMER_PATH, + tg.STATIC_INFO_FKEY: None, + tg.EXTERNAL_INFO_KEY: update_vals, + tg.MAPPING_KEY: None, + tg.NAME_KEY: 'Doctor alarm generator', + tg.NUM_EVENTS: 1 + })] + + return tg.get_trace_generators(test_entity_spec_list) diff --git a/vitrage/tests/mocks/trace_generator.py b/vitrage/tests/mocks/trace_generator.py index 6779197af..ec53e33ea 100644 --- a/vitrage/tests/mocks/trace_generator.py +++ b/vitrage/tests/mocks/trace_generator.py @@ -45,6 +45,7 @@ GENERATOR = 'generator' MOCK_DRIVER_PATH = '%s/mock_configurations/driver' % \ utils.get_resources_dir() DRIVER_AODH_UPDATE_D = 'driver_aodh_update_dynamic.json' +DRIVER_DOCTOR_UPDATE_D = 'driver_doctor_update_dynamic.json' DRIVER_HOST_SNAPSHOT_D = 'driver_host_snapshot_dynamic.json' DRIVER_INST_SNAPSHOT_D = 'driver_inst_snapshot_dynamic.json' DRIVER_INST_SNAPSHOT_S = 'driver_inst_snapshot_static.json' @@ -66,6 +67,7 @@ MOCK_TRANSFORMER_PATH = '%s/mock_configurations/transformer' % \ utils.get_resources_dir() TRANS_AODH_SNAPSHOT_D = 'transformer_aodh_snapshot_dynamic.json' TRANS_AODH_UPDATE_D = 'transformer_aodh_update_dynamic.json' +TRANS_DOCTOR_UPDATE_D = 'transformer_doctor_update_dynamic.json' TRANS_INST_SNAPSHOT_D = 'transformer_inst_snapshot_dynamic.json' TRANS_INST_SNAPSHOT_S = 'transformer_inst_snapshot_static.json' TRANS_HOST_SNAPSHOT_D = 'transformer_host_snapshot_dynamic.json' @@ -108,6 +110,7 @@ class EventTraceGenerator(object): static_info_parsers = \ {DRIVER_AODH_UPDATE_D: _get_aodh_alarm_update_driver_values, + DRIVER_DOCTOR_UPDATE_D: _get_doctor_update_driver_values, DRIVER_INST_SNAPSHOT_D: _get_vm_snapshot_driver_values, DRIVER_INST_UPDATE_D: _get_vm_update_driver_values, DRIVER_HOST_SNAPSHOT_D: _get_host_snapshot_driver_values, @@ -124,6 +127,7 @@ class EventTraceGenerator(object): TRANS_AODH_SNAPSHOT_D: _get_trans_aodh_alarm_snapshot_values, TRANS_AODH_UPDATE_D: _get_trans_aodh_alarm_snapshot_values, + TRANS_DOCTOR_UPDATE_D: _get_trans_doctor_alarm_update_values, TRANS_INST_SNAPSHOT_D: _get_trans_vm_snapshot_values, TRANS_HOST_SNAPSHOT_D: _get_trans_host_snapshot_values, TRANS_ZONE_SNAPSHOT_D: _get_trans_zone_snapshot_values} @@ -243,6 +247,17 @@ def _get_host_snapshot_driver_values(spec): return static_values +def _get_doctor_update_driver_values(spec): + """Generates the static driver values for Doctor monitor notification. + + :param spec: specification of event generation. + :type spec: dict + :return: list of notifications of Doctor monitor + :rtype: list + """ + return [combine_data(None, None, spec.get(EXTERNAL_INFO_KEY, None))] + + def _get_zone_snapshot_driver_values(spec): """Generates the static driver values for each zone. @@ -584,11 +599,11 @@ def _get_trans_zone_snapshot_values(spec): def _get_trans_aodh_alarm_snapshot_values(spec): - """Generates the static transformer values for each vm. + """Generates the dynamic transformer values for Aodh datasource. :param spec: specification of event generation. :type spec: dict - :return: list of static transformer values for each vm. + :return: list of dynamic transformer values for Aodh datasource. :rtype: list """ @@ -621,6 +636,23 @@ def _get_aodh_alarm_update_driver_values(spec): return static_values +def _get_trans_doctor_alarm_update_values(spec): + """Generates the dynamic transformer values for a Doctor alarm + + :param spec: specification of event generation. + :type spec: dict + :return: list of dynamic transformer values for a Doctor alarm + :rtype: list with one alarm + """ + + static_info_re = None + if spec[STATIC_INFO_FKEY] is not None: + static_info_re = utils.load_specs(spec[STATIC_INFO_FKEY]) + + return [combine_data(static_info_re, + None, spec.get(EXTERNAL_INFO_KEY, None))] + + def combine_data(static_info_re, mapping_info, external_info): if external_info: mapping_info = utils.merge_vals(mapping_info, external_info) diff --git a/vitrage/tests/resources/mock_configurations/driver/driver_doctor_update_dynamic.json b/vitrage/tests/resources/mock_configurations/driver/driver_doctor_update_dynamic.json new file mode 100644 index 000000000..64d949f9c --- /dev/null +++ b/vitrage/tests/resources/mock_configurations/driver/driver_doctor_update_dynamic.json @@ -0,0 +1,13 @@ +{ + "time": "2016-04-12T08:00:00.12345", + "type": "compute.host.down", + "details": { + "hostname": "compute-1", + "source": "sample_monitor", + "cause": "link-down", + "severity": "critical", + "status": "down", + "monitor_id": "monitor-1", + "monitor_event_id": "123" + } +} diff --git a/vitrage/tests/resources/mock_configurations/transformer/transformer_doctor_update_dynamic.json b/vitrage/tests/resources/mock_configurations/transformer/transformer_doctor_update_dynamic.json new file mode 100644 index 000000000..63ab6c381 --- /dev/null +++ b/vitrage/tests/resources/mock_configurations/transformer/transformer_doctor_update_dynamic.json @@ -0,0 +1,15 @@ +{ + "time": "2016-04-12T08:00:00.12345", + "type": "compute.host.down", + "vitrage_entity_type" : "doctor", + "vitrage_datasource_action" : "update", + "details": { + "hostname": "compute-1", + "source": "sample_monitor", + "cause": "link-down", + "severity": "critical", + "status": "down", + "monitor_id": "monitor-1", + "monitor_event_id": "123" + } +} diff --git a/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py b/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py new file mode 100644 index 000000000..5e74606cb --- /dev/null +++ b/vitrage/tests/unit/datasources/doctor/test_doctor_driver.py @@ -0,0 +1,134 @@ +# 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 datetime import datetime +from oslo_config import cfg + +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.datasources.doctor.driver import DoctorDriver +from vitrage.datasources.doctor.properties import DoctorDetails +from vitrage.datasources.doctor.properties import DoctorProperties \ + as DoctorProps +from vitrage.datasources.doctor.properties import DoctorStatus +from vitrage.tests import base +from vitrage.tests.mocks import mock_driver + + +# noinspection PyProtectedMember +class DoctorDriverTest(base.BaseTest): + OPTS = [] + + # noinspection PyPep8Naming + @classmethod + def setUpClass(cls): + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group='doctor') + + def test_enrich_event(self): + # Test setup + driver = DoctorDriver(self.conf) + event_type = DoctorProps.HOST_DOWN + + time1 = datetime.now().isoformat() + host1 = 'host1' + event = self._generate_event(time1, host1, DoctorStatus.DOWN) + + # Enrich event + event = driver.enrich_event(event, event_type) + + # Test assertions + self._assert_event_equal(event, event_type, host1, + DoctorStatus.DOWN, time1, time1) + + # Add another event + time2 = datetime.now().isoformat() + host2 = 'host2' + event = self._generate_event(time2, host2, DoctorStatus.DOWN) + + # Enrich event + event = driver.enrich_event(event, event_type) + + # Test assertions + self._assert_event_equal(event, event_type, host2, + DoctorStatus.DOWN, time2, time2) + + # Change the first event to 'up' - should be marked as deleted + time3 = datetime.now().isoformat() + event = self._generate_event(time3, host1, DoctorStatus.UP) + + # Enrich event + event = driver.enrich_event(event, event_type) + + # Test assertions + self._assert_event_equal(event, event_type, host1, + DoctorStatus.UP, time3, time3) + # self._assert_marked_as_deleted(driver, event, True) + + # Send again the second event. The sample time should be new, but the + # update time should remain with its old value (since the state has + # not changed) + time4 = datetime.now().isoformat() + event = self._generate_event(time4, host2, DoctorStatus.DOWN) + + # Enrich event + event = driver.enrich_event(event, event_type) + + # Test assertions + self._assert_event_equal(event, event_type, host2, + DoctorStatus.DOWN, time4, time2) + + # Send again the first event, after it was deleted. Make sure it is + # raised again + time5 = datetime.now().isoformat() + event = self._generate_event(time5, host1, DoctorStatus.DOWN) + + # Enrich event + event = driver.enrich_event(event, event_type) + + # Test assertions + self._assert_event_equal(event, event_type, host1, + DoctorStatus.DOWN, time5, time5) + + @staticmethod + def _generate_event(time, hostname, status): + details = {} + if hostname: + details[DoctorDetails.HOSTNAME] = hostname + if status: + details[DoctorDetails.STATUS] = status + + update_vals = {DoctorProps.DETAILS: details} + if time: + update_vals[DoctorProps.TIME] = time + + generators = mock_driver.simple_doctor_alarm_generators( + update_vals=update_vals) + + return mock_driver.generate_sequential_events_list(generators)[0] + + def _assert_event_equal(self, + event, + expected_event_type, + expected_hostname, + expected_status, + expected_sample_date, + expected_update_date): + self.assertIsNotNone(event, 'No event returned') + self.assertEqual(expected_hostname, + event[DoctorProps.DETAILS][DoctorDetails.HOSTNAME]) + self.assertEqual(expected_status, + event[DoctorProps.DETAILS][DoctorDetails.STATUS]) + self.assertEqual(expected_sample_date, event[DoctorProps.TIME]) + self.assertEqual(expected_update_date, event[DoctorProps.UPDATE_TIME]) + self.assertEqual(expected_event_type, event[DSProps.EVENT_TYPE]) diff --git a/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py b/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py new file mode 100644 index 000000000..a839027e0 --- /dev/null +++ b/vitrage/tests/unit/datasources/doctor/test_doctor_transformer.py @@ -0,0 +1,119 @@ +# 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 datetime import datetime +from oslo_config import cfg +from oslo_log import log as logging + +from vitrage.common.constants import UpdateMethod +from vitrage.datasources.doctor import DOCTOR_DATASOURCE +from vitrage.datasources.doctor.properties import DoctorDetails +from vitrage.datasources.doctor.properties import DoctorProperties \ + as DoctorProps +from vitrage.datasources.doctor.properties import DoctorStatus +from vitrage.datasources.doctor.transformer import DoctorTransformer +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE +from vitrage.datasources.nova.host.transformer import HostTransformer +from vitrage.tests.mocks import mock_transformer +from vitrage.tests.unit.datasources.test_alarm_transformer_base import \ + BaseAlarmTransformerTest + +LOG = logging.getLogger(__name__) + + +# noinspection PyProtectedMember +class DoctorTransformerTest(BaseAlarmTransformerTest): + + OPTS = [ + cfg.StrOpt('update_method', + default=UpdateMethod.PUSH), + ] + + # noinspection PyAttributeOutsideInit,PyPep8Naming + @classmethod + def setUpClass(cls): + cls.transformers = {} + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group=DOCTOR_DATASOURCE) + cls.conf.register_opts(cls.OPTS, group=NOVA_HOST_DATASOURCE) + cls.transformers[DOCTOR_DATASOURCE] = \ + DoctorTransformer(cls.transformers, cls.conf) + cls.transformers[NOVA_HOST_DATASOURCE] = \ + HostTransformer(cls.transformers, cls.conf) + + def test_create_update_entity_vertex(self): + # Test setup + time1 = datetime.now().isoformat() + host1 = 'host1' + event = self._generate_event(time1, host1, DoctorStatus.DOWN) + self.assertIsNotNone(event) + + # Test action + transformer = self.transformers[DOCTOR_DATASOURCE] + wrapper = transformer.transform(event) + + # Test assertions + self._validate_vertex_props(wrapper.vertex, event) + + # Validate the neighbors: only one valid host neighbor + self._validate_host_neighbor(wrapper, + transformer._create_entity_key(event), + host1) + + # Validate the expected action on the graph - update or delete + self._validate_graph_action(wrapper) + + # Create an event with status 'UP' + time2 = datetime.now().isoformat() + host2 = 'host2' + event = self._generate_event(time2, host2, DoctorStatus.UP) + self.assertIsNotNone(event) + + # Test action + transformer = self.transformers[DOCTOR_DATASOURCE] + wrapper = transformer.transform(event) + + # Test assertions + self._validate_vertex_props(wrapper.vertex, event) + self._validate_host_neighbor(wrapper, + transformer._create_entity_key(event), + host2) + self._validate_graph_action(wrapper) + + def _validate_vertex_props(self, vertex, event): + self._validate_alarm_vertex_props(vertex, + event[DoctorProps.TYPE], + DOCTOR_DATASOURCE, + event[DoctorProps.TIME]) + + @staticmethod + def _generate_event(time, hostname, status): + details = {} + if hostname: + details[DoctorDetails.HOSTNAME] = hostname + if status: + details[DoctorDetails.STATUS] = status + + update_vals = {DoctorProps.DETAILS: details} + if time: + update_vals[DoctorProps.TIME] = time + update_vals[DoctorProps.UPDATE_TIME] = time + + generators = mock_transformer.simple_doctor_alarm_generators( + update_vals=update_vals) + + return mock_transformer.generate_random_events_list(generators)[0] + + def _is_erroneous(self, vertex): + return vertex[DoctorDetails.STATUS] == DoctorStatus.DOWN diff --git a/vitrage/tests/unit/datasources/test_alarm_transformer_base.py b/vitrage/tests/unit/datasources/test_alarm_transformer_base.py new file mode 100644 index 000000000..9ebaa8f2b --- /dev/null +++ b/vitrage/tests/unit/datasources/test_alarm_transformer_base.py @@ -0,0 +1,83 @@ +# 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. + +import abc +from oslo_log import log as logging + +from vitrage.common.constants import EdgeLabel +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import GraphAction +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.alarm_properties import AlarmProperties as AlarmProps +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE +from vitrage.tests.unit.datasources.test_transformer_base import \ + BaseTransformerTest + +LOG = logging.getLogger(__name__) + + +# noinspection PyProtectedMember +class BaseAlarmTransformerTest(BaseTransformerTest): + + def _validate_alarm_vertex_props(self, + vertex, + expected_name, + expected_datasource_name, + expected_sample_time): + self._validate_base_vertex_props(vertex, + expected_name, + expected_datasource_name) + + self.assertEqual(EntityCategory.ALARM, vertex[VProps.CATEGORY]) + self.assertEqual(expected_sample_time, vertex[VProps.SAMPLE_TIMESTAMP]) + + if self._is_erroneous(vertex): + self.assertEqual(AlarmProps.ACTIVE_STATE, vertex[VProps.STATE]) + else: + self.assertEqual(AlarmProps.INACTIVE_STATE, vertex[VProps.STATE]) + + def _validate_host_neighbor(self, + wrapper, + alarm_id, + host_name): + + self.assertEqual(1, len(wrapper.neighbors)) + host_neighbor = wrapper.neighbors[0] + + host_transformer = self.transformers[NOVA_HOST_DATASOURCE] + properties = { + VProps.ID: host_name, + VProps.TYPE: NOVA_HOST_DATASOURCE, + VProps.SAMPLE_TIMESTAMP: wrapper.vertex[VProps.SAMPLE_TIMESTAMP], + } + expected_neighbor = host_transformer.\ + create_placeholder_vertex(**properties) + + self.assertEqual(expected_neighbor, host_neighbor.vertex) + + # Validate neighbor edge + edge = host_neighbor.edge + self.assertEqual(edge.source_id, alarm_id) + self.assertEqual(edge.target_id, host_neighbor.vertex.vertex_id) + self.assertEqual(edge.label, EdgeLabel.ON) + + def _validate_graph_action(self, wrapper): + if self._is_erroneous(wrapper.vertex): + self.assertEqual(GraphAction.UPDATE_ENTITY, wrapper.action) + else: + self.assertEqual(GraphAction.DELETE_ENTITY, wrapper.action) + + @abc.abstractmethod + def _is_erroneous(self, vertex): + pass diff --git a/vitrage/tests/unit/datasources/test_transformer_base.py b/vitrage/tests/unit/datasources/test_transformer_base.py new file mode 100644 index 000000000..b243be285 --- /dev/null +++ b/vitrage/tests/unit/datasources/test_transformer_base.py @@ -0,0 +1,32 @@ +# 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 as logging + +from vitrage.common.constants import VertexProperties as VProps +from vitrage.tests import base + +LOG = logging.getLogger(__name__) + + +# noinspection PyProtectedMember +class BaseTransformerTest(base.BaseTest): + + def _validate_base_vertex_props(self, + vertex, + expected_name, + expected_datasource_name): + self.assertFalse(vertex[VProps.IS_PLACEHOLDER]) + self.assertEqual(expected_datasource_name, vertex[VProps.TYPE]) + self.assertEqual(expected_name, vertex[VProps.NAME])