diff --git a/doc/source/contributor/configuration.rst b/doc/source/contributor/configuration.rst index 0fcc82dfe..554b0a94a 100644 --- a/doc/source/contributor/configuration.rst +++ b/doc/source/contributor/configuration.rst @@ -28,6 +28,7 @@ Datasources k8s_datasource nova-config prometheus-datasource + kapacitor-datasource Notifiers --------- diff --git a/doc/source/contributor/kapacitor-datasource.rst b/doc/source/contributor/kapacitor-datasource.rst new file mode 100644 index 000000000..82505d264 --- /dev/null +++ b/doc/source/contributor/kapacitor-datasource.rst @@ -0,0 +1,112 @@ +Kapacitor-Vitrage +================= + +Kapacitor will send alert to vitrage by using [ exec-handle ], send to message queue topic of vitrage. +https://docs.influxdata.com/kapacitor/v1.5/working/alerts/ + + +Installation +------------ + +Copy the 'https://github.com/openstack/vitrage/tree/master/vitrage/datasources/kapacitor/auxliary/kapacitor_vitrage.py' script into the Kapacitor servers. + +.. code-block:: bash + + $ cp kapacitor_vitrage.py /etc/kapacitor/kapacitor_vitrage.py + $ chmod 755 /etc/kapacitor/kapacitor_vitrage.py + + +Configuration +------------- + + + +1. Define topic , which use for alert publish to. Create file ``forward_to_vitrage.yaml``: + + + | topic: forward_to_vitrage + | id: forward_to_vitrage + | kind: exec + | options: + | prog: '/usr/bin/python' + | args: ['/etc/kapacitor/kapacitor_vitrage.py','rabbit://:@controller'] + + **Note:** rabbit://:@controller is Vitrage message bus url, ``rabbit_user:rabbit_pass`` for devstack rabbitmq is ``stackrabbit/secret`` + +Run command to define topic + +.. code-block:: bash + +$ kapacitor define-topic-handler ./forward_to_vitrage.yaml + + +2. Assign your Task to topic, in Tick script define that alert, add in "alert()" step: + + | ... + | alert() + | ... + | .topic('forward_to_vitrage') + +In case your Task already in topic and you don't want to add another, you only need to do: append 'exec handler' to TICK script which define it. + + | ... + | alert() + | ... + | .exec('/usr/bin/python', '/etc/kapacitor/kapacitor_vitrage.py', 'rabbit://:@controller') + +Run command define your task: + +.. code:: + + $ kapacitor define -tick + + +Vitrage configuration: + +1. Add kapacitor to list of datasources in ``/etc/vitrage/vitrage.conf`` + +.. code:: + + [datasources] + types = kapacitor,zabbix,nova.host,nova.instance,nova.zone,static_physical,aodh,cinder.volume,neutron.network,neutron.port,heat.stack + +2. Add section to ``/etc/vitrage/vitrage.conf`` + +.. code:: + + [kapacitor] + config_file = /etc/vitrage/kapacitor_conf.yaml + +3. Create ``/etc/vitrage/kapacitor_conf.yaml`` with this content + +.. code :: + + kapacitor: + - alert: + host: cloud.compute1 # hostname of host been raised alarm + vitrage_resource: + type: nova.host # resource type of enity vitrage + name: compute-1 # resource name of enity vitrage + - alert: + host: compute-(.*) + vitrage_resource: + type: nova.host + name: ${kapacitor_host} + - alert: + host: (.*) + vitrage_resource: + type: nova.instance + name: ${kapacitor_host} + +In example: +alarm on host have hostname `cloud.compute1` will map to resource name `compute-1`, + +alarm on host have hostname `compute-99` will map to resource name `compute-99` + +Another alarm, like alarm on instance will map with resource type ``nova.instance`` and name equal with hostname of instance + +4. Restart vitrage service in devstack/openstack + +DONE +---- + diff --git a/etc/vitrage/datasources_values/kapacitor.yaml b/etc/vitrage/datasources_values/kapacitor.yaml new file mode 100644 index 000000000..533b7a14a --- /dev/null +++ b/etc/vitrage/datasources_values/kapacitor.yaml @@ -0,0 +1,18 @@ +category: ALARM +values: + - aggregated values: + priority: 40 + original values: + - name: critical + operational_value: CRITICAL + - aggregated values: + priority: 20 + original values: + - name: warning + operational_value: WARNING + - aggregated values: + priority: 10 + original values: + - name: OK + operational_value: OK + diff --git a/releasenotes/notes/kapacitor_datasource-c0c9563bb52ff1f1.yaml b/releasenotes/notes/kapacitor_datasource-c0c9563bb52ff1f1.yaml new file mode 100644 index 000000000..25e68318c --- /dev/null +++ b/releasenotes/notes/kapacitor_datasource-c0c9563bb52ff1f1.yaml @@ -0,0 +1,6 @@ +--- +features: + - A new ``Kapacitor Datasource`` was added, to handle alerts coming + from Kapacitor. Kapacitor is an alarming engine in the TICK Stack. + It is build on an Open Source core, processing metric of host or + instance store in InfluxDB to export alerts. diff --git a/vitrage/datasources/kapacitor/__init__.py b/vitrage/datasources/kapacitor/__init__.py new file mode 100644 index 000000000..239921cb8 --- /dev/null +++ b/vitrage/datasources/kapacitor/__init__.py @@ -0,0 +1,42 @@ +# Copyright 2019 - Viettel +# +# 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 DatasourceOpts as DSOpts +from vitrage.common.constants import UpdateMethod + +KAPACITOR_DATASOURCE = 'kapacitor' + +OPTS = [ + cfg.StrOpt(DSOpts.TRANSFORMER, + default='vitrage.datasources.kapacitor.transformer.' + 'KapacitorTransformer', + help='Kapacitor transformer class path', + required=True), + cfg.StrOpt(DSOpts.DRIVER, + default='vitrage.datasources.kapacitor.driver.' + 'KapacitorDriver', + help='Kapacitor driver class path', + required=True), + cfg.StrOpt(DSOpts.UPDATE_METHOD, + default=UpdateMethod.PUSH, + help='None: updates only via Vitrage periodic snapshots.' + 'Pull: updates periodically.' + 'Push: updates by getting notifications from the' + ' datasource itself.', + required=True), + cfg.StrOpt(DSOpts.CONFIG_FILE, default='/etc/vitrage/kapacitor_conf.yaml', + help='Kapacitor configuration file') +] diff --git a/vitrage/datasources/kapacitor/auxiliary/kapacitor_vitrage.py b/vitrage/datasources/kapacitor/auxiliary/kapacitor_vitrage.py new file mode 100644 index 000000000..01000cb4e --- /dev/null +++ b/vitrage/datasources/kapacitor/auxiliary/kapacitor_vitrage.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python + +# coding: utf-8 + +# Copyright 2019 - Viettel +# +# 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 argparse +import json +import logging +from logging.handlers import RotatingFileHandler +from oslo_config import cfg +import oslo_messaging as messaging +from oslo_utils import uuidutils +import socket +import sys + +''' +Expected input: +Send To: rabbit://userrabbit:passrabbit@rabbit_host:5672/ +EVENT_TYPE: {ALARM.STATUS} || kapacitor.alarm.critical warning info or ok +Alarm: + id: mem high-host=controller + message: mem high + details: {{ .Level }} {{alarm_name}}... + times: 2019-04-10T12:18:00Z + duration: 0 + priority: CRITICAL + previousLevel: OK + host: host1 +''' + + +LOG_FILE = '/var/log/kapacitor/kapacitor_vitrage.log' +LOG_MAX_SIZE = 10000000 +LOG_FORMAT = '%(asctime)s.%(msecs).03d %(name)s[%(process)d] %(threadName)s %' \ + '(levelname)s - %(message)s' +LOG_DATE_FMT = '%Y.%m.%d %H:%M:%S' +KAPACITOR_EVENT_TYPE = 'kapacitor.alarm' + +debug = False + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('sendto', help='url') + args = parser.parse_args() + data = sys.stdin.readlines()[0] + transport_url = args.sendto + transport = messaging.get_notification_transport(cfg.CONF, transport_url) + + driver = 'messagingv2' + publisher = 'kapacitor_%s' % socket.gethostname() + notifier = messaging.Notifier(transport, + driver=driver, + publisher_id=publisher, + topics=['vitrage_notifications']) + alarm = json.loads(data) + host = alarm['data']['series'][0]['tags']['host'] + priority = alarm['level'].lower() + alarm.update({'host': host, + 'priority': priority}) + alarm.pop('data', None) + alarm_status = alarm['level'].lower() + event_type = '%s.%s' % (KAPACITOR_EVENT_TYPE, alarm_status) + logging.info('Send to: %s', transport_url) + logging.info('BODY:\n----\n%s\n', data) + logging.info('PUBLISHER: %s', publisher) + logging.info('EVENT_TYPE: %s', event_type) + logging.info('\nALARM:\n%s', alarm) + notifier.info(ctxt={'message_id': uuidutils.generate_uuid(), + 'publisher_id': publisher}, + event_type=event_type, + payload=alarm) + logging.info('MESSAGE SENT..') + + +if __name__ == '__main__': + + log = logging.getLogger() + + if debug: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + + handler = RotatingFileHandler(filename=LOG_FILE, + maxBytes=LOG_MAX_SIZE, + backupCount=3) + fmt = logging.Formatter(LOG_FORMAT, LOG_DATE_FMT) + handler.setFormatter(fmt) + log.addHandler(handler) + + logging.info('***----------Script start-----------***') + try: + main() + except Exception as e: + logging.exception('MESSAGE WAS NOT SENT - %s' % e) diff --git a/vitrage/datasources/kapacitor/config.py b/vitrage/datasources/kapacitor/config.py new file mode 100644 index 000000000..f7e8dd9a0 --- /dev/null +++ b/vitrage/datasources/kapacitor/config.py @@ -0,0 +1,91 @@ +# Copyright 2019 - Viettel +# +# 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 re + +from oslo_log import log + +from vitrage.common.constants import DatasourceOpts as DSOpts +from vitrage.utils import file as file_utils + +LOG = log.getLogger(__name__) +KAPACITOR_HOST = 'kapacitor_host' +KAPACITOR = 'kapacitor' +HOST = 'host' +TYPE = 'type' +NAME = 'name' +ALERT = 'alert' +VITRAGE_RESOURCE = 'vitrage_resource' + + +class KapacitorConfig(object): + def __init__(self, conf): + try: + kapacitor_config_file = conf.kapacitor[DSOpts.CONFIG_FILE] + kapacitor_config = file_utils.load_yaml_file(kapacitor_config_file) + kapacitor = kapacitor_config[KAPACITOR] + + self.mappings = [self._create_mapping(config) + for config in kapacitor] + except Exception: + LOG.exception('Failed in init.') + self.mappings = [] + + @staticmethod + def _create_mapping(config): + return KapacitorHostMapping(config[ALERT][HOST], + config[VITRAGE_RESOURCE][TYPE], + config[VITRAGE_RESOURCE][NAME]) + + def get_vitrage_resource(self, kapacitor_host): + """Get Resource type and name for the given kapacitor host name + + Go over the configuration mappings one by one, and return the resource + by the first mapping that applies to kapacitor host name. + + :param kapacitor_host: kapacitor host name + :return: Vitrage (resource type, resource name) + """ + for mapping in self.mappings: + mapped_resource = mapping.map(kapacitor_host) + if mapped_resource: + return mapped_resource + + return None + + +class KapacitorHostMapping(object): + KAPACITOR_HOST_NAME = '${' + KAPACITOR_HOST + '}' + + def __init__(self, kapacitor_host_regexp, resource_type, resource_name): + self.kapacitor_host_regexp = re.compile(kapacitor_host_regexp) + self.resource_type = resource_type + self.resource_name = resource_name + + def map(self, kapacitor_host): + """Check if the mapping applies to this service + + :param kapacitor_host: kapacitor host name + :return: a tuple of (resource type, resource name) + In case kapacitor_host_regexp is ${kapacitor_host}, + return kapacitor host name as the resource name + """ + + if kapacitor_host and self.kapacitor_host_regexp.match(kapacitor_host): + resource_name = \ + kapacitor_host if self.resource_name == self.KAPACITOR_HOST_NAME \ + else self.resource_name + return self.resource_type, resource_name + else: + return None diff --git a/vitrage/datasources/kapacitor/driver.py b/vitrage/datasources/kapacitor/driver.py new file mode 100644 index 000000000..ce5d98c49 --- /dev/null +++ b/vitrage/datasources/kapacitor/driver.py @@ -0,0 +1,94 @@ +# Copyright 2019 - Viettel +# +# 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.kapacitor.config import KapacitorConfig +from vitrage.datasources.kapacitor import KAPACITOR_DATASOURCE +from vitrage.datasources.kapacitor.properties import KapacitorProperties \ + as KProps +from vitrage.datasources.kapacitor.properties import KapacitorState + +LOG = log.getLogger(__name__) + + +class KapacitorDriver(AlarmDriverBase): + ServiceKey = namedtuple('ServiceKey', ['hostname', 'alarmid']) + conf_map = None + + def __init__(self, conf): + super(KapacitorDriver, self).__init__() + self.cfg = conf + + if not KapacitorDriver.conf_map: + self.conf_map = KapacitorConfig(conf) + self._client = None + + @staticmethod + def get_event_types(): + return ['kapacitor.alarm.ok', + 'kapacitor.alarm.info', + 'kapacitor.alarm.warning', + 'kapacitor.alarm.critical'] + + def _vitrage_type(self): + return KAPACITOR_DATASOURCE + + def _alarm_key(self, alarm): + return self.ServiceKey(hostname=alarm[KProps.RESOURCE_NAME], + alarmid=alarm[KProps.ID]) + + def _enrich_alarms(self, alarms): + """Enrich kapacitor alarm using kapacitor configuration file + + Converting Kapacitor host name to Vitrage resource type and name + It is function of get_all for pulling method + Not implement yet + """ + pass + + def enrich_event(self, event, event_type): + event[DSProps.EVENT_TYPE] = event_type + + kapacitor_host = event[KProps.HOST] + vitrage_resource = self.conf_map.get_vitrage_resource(kapacitor_host) + event[KProps.RESOURCE_TYPE] = \ + vitrage_resource[0] if vitrage_resource else None + event[KProps.RESOURCE_NAME] = \ + vitrage_resource[1] if vitrage_resource else None + return KapacitorDriver.make_pickleable([event], KAPACITOR_DATASOURCE, + DatasourceAction.UPDATE)[0] + + def _is_erroneous(self, alarm): + return alarm and alarm[KProps.PRIORITY] != KapacitorState.OK + + def _status_changed(self, new_alarm, old_alarm): + return new_alarm and old_alarm and \ + not new_alarm[KProps.PRIORITY] == old_alarm[KProps.PRIORITY] + + def _is_valid(self, alarm): + return alarm[KProps.RESOURCE_TYPE] is not None and \ + alarm[KProps.RESOURCE_NAME] is not None + + def _get_alarms(self): + """Query all alarm and send to vitrage + + Not implement yet + """ + return [] diff --git a/vitrage/datasources/kapacitor/properties.py b/vitrage/datasources/kapacitor/properties.py new file mode 100644 index 000000000..9f2604e0e --- /dev/null +++ b/vitrage/datasources/kapacitor/properties.py @@ -0,0 +1,32 @@ +# Copyright 2019 - Viettel +# +# 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 KapacitorProperties(object): + ID = 'id' + RESOURCE_TYPE = 'resource_type' + RESOURCE_NAME = 'resource_name' + DETAILS = 'details' + STATUS = 'status' + HOST = 'host' + PRIORITY = 'priority' + TIME = 'time' + MESSAGE = 'message' + + +class KapacitorState(object): + OK = 'ok' + INFO = 'info' + WARNING = 'warning' + CRITICAL = 'critical' diff --git a/vitrage/datasources/kapacitor/transformer.py b/vitrage/datasources/kapacitor/transformer.py new file mode 100644 index 000000000..4428ae265 --- /dev/null +++ b/vitrage/datasources/kapacitor/transformer.py @@ -0,0 +1,110 @@ +# Copyright 2019 - Viettel +# +# 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_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.kapacitor import KAPACITOR_DATASOURCE +from vitrage.datasources.kapacitor.properties import KapacitorProperties \ + as KProps +from vitrage.datasources.kapacitor.properties import KapacitorState +from vitrage.datasources import transformer_base as tbase +import vitrage.graph.utils as graph_utils + +LOG = logging.getLogger(__name__) + + +class KapacitorTransformer(AlarmTransformerBase): + + def __init__(self, transformers, conf): + super(KapacitorTransformer, self).__init__(transformers, conf) + + def _create_snapshot_entity_vertex(self, entity_event): + return self._create_vertex(entity_event) + + def _create_update_entity_vertex(self, entity_event): + return self._create_vertex(entity_event) + + def _create_snapshot_neighbors(self, entity_event): + return self._create_kapacitor_neighbors(entity_event) + + def _create_update_neighbors(self, entity_event): + return self._create_kapacitor_neighbors(entity_event) + + def _create_entity_key(self, entity_event): + """the unique key of this entity""" + entity_type = entity_event[DSProps.ENTITY_TYPE] + alarm_id = entity_event[KProps.ID] + resource_name = entity_event[KProps.RESOURCE_NAME] + return tbase.build_key((EntityCategory.ALARM, + entity_type, + resource_name, + alarm_id)) + + @staticmethod + def get_vitrage_type(): + return KAPACITOR_DATASOURCE + + def _create_vertex(self, entity_event): + update_timestamp = str(datetime.strptime( + entity_event[KProps.TIME], tbase.TIMESTAMP_FORMAT)) + + vitrage_sample_timestamp = entity_event[DSProps.SAMPLE_DATE] + + metadata = { + VProps.NAME: entity_event[KProps.MESSAGE], + VProps.SEVERITY: entity_event[KProps.PRIORITY], + VProps.RAWTEXT: entity_event[KProps.DETAILS], + VProps.RESOURCE_NAME: entity_event[KProps.RESOURCE_NAME], + } + + return graph_utils.create_vertex( + self._create_entity_key(entity_event), + vitrage_category=EntityCategory.ALARM, + vitrage_type=entity_event[DSProps.ENTITY_TYPE], + vitrage_sample_timestamp=vitrage_sample_timestamp, + update_timestamp=update_timestamp, + entity_state=self._get_alarm_state(entity_event), + metadata=metadata) + + def _ok_status(self, entity_event): + return entity_event[KProps.PRIORITY] == KapacitorState.OK + + def _create_kapacitor_neighbors(self, entity_event): + graph_neighbors = entity_event.get(self.QUERY_RESULT, []) + return [self._create_neighbor( + entity_event, + graph_neighbor[VProps.ID], + graph_neighbor[VProps.VITRAGE_TYPE], + EdgeLabel.ON, + neighbor_category=EntityCategory.RESOURCE) + for graph_neighbor in graph_neighbors] + + @staticmethod + def get_enrich_query(event): + resource_type = event.get(KProps.RESOURCE_TYPE) + resource_name = event.get(KProps.RESOURCE_NAME) + + if resource_type and resource_name: + return {VProps.NAME: resource_name, + VProps.VITRAGE_TYPE: resource_type} + + return None diff --git a/vitrage/tests/mocks/mock_driver.py b/vitrage/tests/mocks/mock_driver.py index e7eb86a90..ee8d8575c 100644 --- a/vitrage/tests/mocks/mock_driver.py +++ b/vitrage/tests/mocks/mock_driver.py @@ -618,3 +618,24 @@ def simple_k8s_nodes_generators(nodes_num, snapshot_events=0): } ) return tg.get_trace_generators(test_entity_spec_list) + + +def simple_kapacitor_alarm_generators(update_vals=None): + """A function for returning Kapacitor alarm event generators. + + Returns generators for a given number of Kapacitor 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_KAPACITOR_UPDATE_D, + tg.STATIC_INFO_FKEY: None, + tg.EXTERNAL_INFO_KEY: update_vals, + tg.MAPPING_KEY: None, + tg.NAME_KEY: 'Kapacitor alarm generator', + tg.NUM_EVENTS: 1 + })] + + return tg.get_trace_generators(test_entity_spec_list) diff --git a/vitrage/tests/mocks/mock_transformer.py b/vitrage/tests/mocks/mock_transformer.py index ec5d6b605..2637ceffa 100644 --- a/vitrage/tests/mocks/mock_transformer.py +++ b/vitrage/tests/mocks/mock_transformer.py @@ -214,6 +214,18 @@ def simple_collectd_alarm_generators(update_vals=None): tg.TRANS_COLLECTD_UPDATE_D, update_vals) +def simple_kapacitor_alarm_generators(update_vals=None): + """A function for returning Kapacitor alarm event generators. + + Returns generators for a given number of Kapacitor alarms. + + :param update_vals: preset values for ALL update events + :return: generators for alarms as specified + """ + return _simple_alarm_generators('Kapacitor', + tg.TRANS_KAPACITOR_UPDATE_D, update_vals) + + def simple_prometheus_alarm_generators(update_vals=None): """A function for returning Prometheus alert event generators. diff --git a/vitrage/tests/mocks/trace_generator.py b/vitrage/tests/mocks/trace_generator.py index fe874e2b8..578e1506f 100644 --- a/vitrage/tests/mocks/trace_generator.py +++ b/vitrage/tests/mocks/trace_generator.py @@ -50,6 +50,7 @@ MOCK_DRIVER_PATH = '%s/mock_configurations/driver' % \ DRIVER_AODH_UPDATE_D = 'driver_aodh_update_dynamic.json' DRIVER_DOCTOR_UPDATE_D = 'driver_doctor_update_dynamic.json' DRIVER_COLLECTD_UPDATE_D = 'driver_collectd_update_dynamic.json' +DRIVER_KAPACITOR_UPDATE_D = 'driver_kapacitor_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' @@ -80,6 +81,7 @@ 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_COLLECTD_UPDATE_D = 'transformer_collectd_update_dynamic.json' +TRANS_KAPACITOR_UPDATE_D = 'transformer_kapacitor_update_dynamic.json' TRANS_PROMETHEUS_UPDATE_D = 'transformer_prometheus_update_dynamic.json' TRANS_INST_SNAPSHOT_D = 'transformer_inst_snapshot_dynamic.json' TRANS_HOST_SNAPSHOT_D = 'transformer_host_snapshot_dynamic.json' @@ -124,6 +126,7 @@ class EventTraceGenerator(object): {DRIVER_AODH_UPDATE_D: _get_aodh_alarm_update_driver_values, DRIVER_DOCTOR_UPDATE_D: _get_simple_update_driver_values, DRIVER_COLLECTD_UPDATE_D: _get_simple_update_driver_values, + DRIVER_KAPACITOR_UPDATE_D: _get_simple_update_driver_values, DRIVER_KUBE_SNAPSHOT_D: _get_k8s_node_snapshot_driver_values, DRIVER_INST_SNAPSHOT_D: _get_vm_snapshot_driver_values, DRIVER_INST_UPDATE_LEGACY_D: _get_vm_update_legacy_driver_values, @@ -149,6 +152,7 @@ class EventTraceGenerator(object): TRANS_AODH_UPDATE_D: _get_trans_aodh_alarm_snapshot_values, TRANS_DOCTOR_UPDATE_D: _get_simple_trans_alarm_update_values, TRANS_COLLECTD_UPDATE_D: _get_simple_trans_alarm_update_values, + TRANS_KAPACITOR_UPDATE_D: _get_simple_trans_alarm_update_values, TRANS_PROMETHEUS_UPDATE_D: _get_simple_trans_alarm_update_values, TRANS_INST_SNAPSHOT_D: _get_trans_vm_snapshot_values, TRANS_HOST_SNAPSHOT_D: _get_trans_host_snapshot_values, diff --git a/vitrage/tests/resources/kapacitor/kapacitor_conf.yaml b/vitrage/tests/resources/kapacitor/kapacitor_conf.yaml new file mode 100644 index 000000000..651f81dbb --- /dev/null +++ b/vitrage/tests/resources/kapacitor/kapacitor_conf.yaml @@ -0,0 +1,16 @@ +kapacitor: + - alert: + host: cloud.compute1 + vitrage_resource: + type: nova.host + name: compute-1 + - alert: + host: compute-(.*) + vitrage_resource: + type: nova.host + name: ${kapacitor_host} + - alert: + host: (.*) + vitrage_resource: + type: nova.instance + name: ${kapacitor_host} diff --git a/vitrage/tests/resources/mock_configurations/driver/driver_kapacitor_update_dynamic.json b/vitrage/tests/resources/mock_configurations/driver/driver_kapacitor_update_dynamic.json new file mode 100644 index 000000000..4666b63fa --- /dev/null +++ b/vitrage/tests/resources/mock_configurations/driver/driver_kapacitor_update_dynamic.json @@ -0,0 +1,12 @@ +{ + "level": "CRITICAL", + "priority": "critical", + "host": "compute-1", + "details": "DETAIL INFO :: LEVEL: CRITICAL - ID: mem high-host=compute-1 - NAME : mem - VALUE: 9.746241910865896 - TASKNAME: chronograf-v1-2d1fad8a-1c52-4eca-8721-c7af36faf7c - GROUP: host=compute-1 - TAGS: map[host:compute-1] - TIME: 2019-04-22 08:11:00 +0000 UTC", + "time": "2019-04-22T08:11:00Z", + "duration": 0, + "message": "mem high", + "id": "mem high-host=compute-1", + "recoverable": "True", + "previousLevel": "OK" +} diff --git a/vitrage/tests/resources/mock_configurations/transformer/transformer_kapacitor_update_dynamic.json b/vitrage/tests/resources/mock_configurations/transformer/transformer_kapacitor_update_dynamic.json new file mode 100644 index 000000000..cfa399999 --- /dev/null +++ b/vitrage/tests/resources/mock_configurations/transformer/transformer_kapacitor_update_dynamic.json @@ -0,0 +1,18 @@ +{ + "level": "CRITICAL", + "priority": "critical", + "host": "compute-1", + "vitrage_entity_type" : "kapacitor", + "vitrage_datasource_name": "kapacitor", + "vitrage_datasource_action" : "update", + "vitrage_sample_date": "2019-04-22T06:31:50.094836", + "resource_type": "nova.host", + "resource_name": "compute-1", + "details": "DETAIL INFO :: LEVEL: CRITICAL - ID: mem high-host=compute-1 - NAME : mem - VALUE: 9.746241910865896 - TASKNAME: chronograf-v1-2d1fad8a-1c52-4eca-8721-c7af36faf7c - GROUP: host=compute-1 - TAGS: map[host:compute-1] - TIME: 2019-04-22 08:11:00 +0000 UTC", + "time": "2019-04-22T08:11:00Z", + "duration": 0, + "message": "mem high", + "id": "mem high-host=compute-1", + "recoverable": "True", + "previousLevel": "OK" +} diff --git a/vitrage/tests/unit/datasources/kapacitor/__init__.py b/vitrage/tests/unit/datasources/kapacitor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_configuration.py b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_configuration.py new file mode 100644 index 000000000..bd2d1b476 --- /dev/null +++ b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_configuration.py @@ -0,0 +1,87 @@ +# Copyright 2019 - Viettel +# +# 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 DatasourceOpts as DSOpts +from vitrage.datasources.kapacitor.config import KapacitorConfig +from vitrage.datasources.kapacitor import KAPACITOR_DATASOURCE +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE +from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE +from vitrage.tests import base +from vitrage.tests.mocks import utils + + +class TestKapacitorConfig(base.BaseTest): + + OPTS = [ + cfg.StrOpt(DSOpts.TRANSFORMER, + default='vitrage.datasources.kapacitor.transformer.' + 'KapacitorTransformer', + help='Kapacitor data source transformer class path', + required=True), + cfg.StrOpt(DSOpts.DRIVER, + default='vitrage.datasources.kapacitor.driver.' + 'KapacitorDriver', + help='Kapacitor driver class path', + required=True), + cfg.StrOpt(DSOpts.CONFIG_FILE, + help='Kapacitor configuration file', + default=utils.get_resources_dir() + + '/kapacitor/kapacitor_conf.yaml'), + ] + + # noinspection PyPep8Naming + @classmethod + def setUpClass(cls): + super(TestKapacitorConfig, cls).setUpClass() + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group=KAPACITOR_DATASOURCE) + + def test_get_vitrage_resource(self): + """Test the resource returned after processing a list of mappings + + :return: + """ + # Action + kapacitor_conf = KapacitorConfig(self.conf) + + # Test assertions + mapped_resource = kapacitor_conf.get_vitrage_resource(None) + self.assertIsNone(mapped_resource, 'expected None') + + mapped_resource = kapacitor_conf.get_vitrage_resource('') + self.assertIsNone(mapped_resource, 'expected None') + + mapped_resource = kapacitor_conf.get_vitrage_resource('cloud.compute1') + self.assertIsNotNone(mapped_resource, 'expected Not None') + self.assertEqual(NOVA_HOST_DATASOURCE, mapped_resource[0]) + self.assertEqual('compute-1', mapped_resource[1]) + + mapped_resource = kapacitor_conf.get_vitrage_resource('compute-2') + self.assertIsNotNone(mapped_resource, 'expected Not None') + self.assertEqual(NOVA_HOST_DATASOURCE, mapped_resource[0]) + self.assertEqual('compute-2', mapped_resource[1]) + + mapped_resource = kapacitor_conf.get_vitrage_resource('instance-1') + self.assertIsNotNone(mapped_resource, 'expected Not None') + self.assertEqual(NOVA_INSTANCE_DATASOURCE, mapped_resource[0]) + self.assertEqual('instance-1', mapped_resource[1]) + + @staticmethod + def _assert_equals(mapping1, mapping2): + return mapping1.kapacitor_host_regexp.pattern == \ + mapping2.kapacitor_host_regexp.pattern and \ + mapping1.resource_type == mapping2.resource_type and \ + mapping1.resource_name == mapping2.resource_name diff --git a/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_driver.py b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_driver.py new file mode 100644 index 000000000..efabc6442 --- /dev/null +++ b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_driver.py @@ -0,0 +1,120 @@ +# Copyright 2019 - Viettel +# +# 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 DatasourceOpts as DSOpts +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.datasources.kapacitor.driver import KapacitorDriver +from vitrage.datasources.kapacitor import KAPACITOR_DATASOURCE +from vitrage.datasources.kapacitor.properties \ + import KapacitorProperties as KProps +from vitrage.datasources.kapacitor.properties \ + import KapacitorState as KState +from vitrage.tests import base +from vitrage.tests.mocks import mock_driver +from vitrage.tests.mocks import utils + +# notification alarm input +HOST = 'compute-1' +ALARM_PRIntORIY = 'critical' +ALARM_EVENT_TYPE = 'kapacitor.alarm.critical' + +# match result +EXPECTED_RESOURCE_TYPE = 'nova.host' +EXPECTED_RESOURCE_NAME = 'compute1' +EXPECTED_EVENT_PRIORIY = 'critical' +EXPECTED_EVENT_TYPE = 'kapacitor.alarm.critical' + + +class TestKapacitorDriver(base.BaseTest): + OPTS = [ + cfg.StrOpt(DSOpts.CONFIG_FILE, + help='Kapacitor configuration file', + default=utils.get_resources_dir() + + '/kapacitor/kapacitor_conf.yaml'), + ] + + # noinspection PyPep8Naming + @classmethod + def setUpClass(cls): + super(TestKapacitorDriver, cls).setUpClass() + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group=KAPACITOR_DATASOURCE) + + # noinspection PyAttributeOutsideInit + def setUp(self): + super(TestKapacitorDriver, self).setUp() + self.driver = KapacitorDriver(self.conf) + + def test_enrich_event(self): + # Test event on host + # Setup + input_data = {KProps.HOST: 'compute-1', + KProps.PRIORITY: 'CPU utilization', + ALARM_EVENT_TYPE: KState.CRITICAL} + expected_data = {DSProps.EVENT_TYPE: KState.CRITICAL, + KProps.RESOURCE_NAME: 'compute-1', + KProps.RESOURCE_TYPE: 'nova.host', + KProps.PRIORITY: 'CPU utilization'} + event = self._generate_event(input_data[KProps.HOST], + input_data[KProps.PRIORITY]) + # Action + event = self.driver.enrich_event(event, + input_data[ALARM_EVENT_TYPE]) + # Test assertions + self._assert_event_equal(event, expected_data) + + # Test event on instance + # Setup + input_data = {KProps.HOST: 'node1-vm', + KProps.PRIORITY: 'CPU utilization', + ALARM_EVENT_TYPE: KState.CRITICAL} + expected_data = {DSProps.EVENT_TYPE: KState.CRITICAL, + KProps.RESOURCE_NAME: 'node1-vm', + KProps.RESOURCE_TYPE: 'nova.instance', + KProps.PRIORITY: 'CPU utilization'} + event = self._generate_event(input_data[KProps.HOST], + input_data[KProps.PRIORITY]) + # Action + event = self.driver.enrich_event(event, + input_data[ALARM_EVENT_TYPE]) + # Test assertions + self._assert_event_equal(event, expected_data) + + @staticmethod + def _generate_event(hostname, priority): + update_vals = {} + if hostname: + update_vals[KProps.HOST] = hostname + if priority: + update_vals[KProps.PRIORITY] = priority + + generators = mock_driver.simple_kapacitor_alarm_generators( + update_vals=update_vals) + + return mock_driver.generate_sequential_events_list(generators)[0] + + def _assert_event_equal(self, + event1, + event2): + self.assertIsNotNone(event1, 'No event returned') + self.assertEqual(event1[DSProps.EVENT_TYPE], + event2[DSProps.EVENT_TYPE]) + self.assertEqual(event1[KProps.RESOURCE_NAME], + event2[KProps.RESOURCE_NAME]) + self.assertEqual(event1[KProps.RESOURCE_TYPE], + event2[KProps.RESOURCE_TYPE]) + self.assertEqual(event1[KProps.PRIORITY], + event2[KProps.PRIORITY]) diff --git a/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_transformer.py b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_transformer.py new file mode 100644 index 000000000..cd45c3c61 --- /dev/null +++ b/vitrage/tests/unit/datasources/kapacitor/test_kapacitor_transformer.py @@ -0,0 +1,175 @@ +# Copyright 2019 - Viettel +# +# 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 uuid + +from oslo_config import cfg +from oslo_log import log as logging + +from vitrage.common.constants import DatasourceOpts as DSOpts +from vitrage.common.constants import DatasourceProperties as DSProps +from vitrage.common.constants import EntityCategory +from vitrage.common.constants import UpdateMethod +from vitrage.common.constants import VertexProperties as VProps +from vitrage.datasources.kapacitor import KAPACITOR_DATASOURCE +from vitrage.datasources.kapacitor.properties import KapacitorProperties \ + as KProps +from vitrage.datasources.kapacitor.properties import KapacitorState \ + as KState +from vitrage.datasources.kapacitor.transformer import KapacitorTransformer +from vitrage.datasources.nova.host import NOVA_HOST_DATASOURCE +from vitrage.datasources.nova.host.transformer import HostTransformer +from vitrage.datasources.nova.instance import NOVA_INSTANCE_DATASOURCE +from vitrage.datasources.nova.instance.transformer import InstanceTransformer +from vitrage.datasources.transformer_base import TransformerBase +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 TestKapacitorTransformer(BaseAlarmTransformerTest): + + OPTS = [ + cfg.StrOpt(DSOpts.UPDATE_METHOD, + default=UpdateMethod.PUSH), + ] + + # noinspection PyAttributeOutsideInit,PyPep8Naming + @classmethod + def setUpClass(cls): + super(TestKapacitorTransformer, cls).setUpClass() + cls.transformers = {} + cls.conf = cfg.ConfigOpts() + cls.conf.register_opts(cls.OPTS, group=KAPACITOR_DATASOURCE) + cls.transformers[KAPACITOR_DATASOURCE] = \ + KapacitorTransformer(cls.transformers, cls.conf) + cls.transformers[NOVA_INSTANCE_DATASOURCE] = \ + InstanceTransformer(cls.transformers, cls.conf) + cls.transformers[NOVA_HOST_DATASOURCE] = \ + HostTransformer(cls.transformers, cls.conf) + + def test_create_entity_key(self): + LOG.debug('Test get key from nova host transformer') + + # Test setup + host = 'compute-1' + resource_name = 'compute-1' + resource_type = 'nova.host' + update_vals = {KProps.HOST: host, + KProps.RESOURCE_TYPE: resource_type, + KProps.RESOURCE_NAME: resource_name} + + event = self._generate_event(update_vals) + transformer = KapacitorTransformer(self.transformers, self.conf) + self.assertIsNotNone(event) + + # Test action + observed_key = transformer._create_entity_key(event) + + # Test assertions + observed_key_fields = observed_key.split( + TransformerBase.KEY_SEPARATOR) + + self.assertEqual(EntityCategory.ALARM, observed_key_fields[0]) + self.assertEqual(event[DSProps.ENTITY_TYPE], observed_key_fields[1]) + self.assertEqual(event[KProps.RESOURCE_NAME], + observed_key_fields[2]) + self.assertEqual(event[KProps.ID], + observed_key_fields[3]) + + def test_create_update_entity_vertex(self): + # Test setup + host1 = 'host1' + instance_id = uuid.uuid4().hex + event_on_host = self._generate_event_on_host(host1) + event_on_instance = self._generate_event_on_instance(host1, + instance_id) + self.assertIsNotNone(event_on_host) + self.assertIsNotNone(event_on_instance) + + # Test action + transformer = self.transformers[KAPACITOR_DATASOURCE] + wrapper_for_host = transformer.transform(event_on_host) + wrapper_for_instance = transformer.transform(event_on_instance) + + # Test assertions + self._validate_vertex_props(wrapper_for_host.vertex, event_on_host) + self._validate_vertex_props(wrapper_for_instance.vertex, + event_on_instance) + + # Validate the neighbors: only one valid host neighbor + host_entity_key = transformer._create_entity_key(event_on_host) + host_entity_uuid = \ + transformer.uuid_from_deprecated_vitrage_id(host_entity_key) + + instance_entity_key = transformer._create_entity_key(event_on_instance) + instance_entity_uuid = \ + transformer.uuid_from_deprecated_vitrage_id(instance_entity_key) + + self._validate_host_neighbor(wrapper_for_host, + host_entity_uuid, + host1) + + self._validate_instance_neighbor(wrapper_for_instance, + instance_entity_uuid, + instance_id) + + # Validate the expected action on the graph - update or delete + self._validate_graph_action(wrapper_for_host) + self._validate_graph_action(wrapper_for_instance) + + def _validate_vertex_props(self, vertex, event): + self._validate_alarm_vertex_props( + vertex, event[KProps.MESSAGE], + KAPACITOR_DATASOURCE, event[DSProps.SAMPLE_DATE]) + + @staticmethod + def _generate_event(update_vals): + generators = mock_transformer.simple_kapacitor_alarm_generators( + update_vals=update_vals) + + return mock_transformer.generate_random_events_list(generators)[0] + + def _generate_event_on_host(self, hostname): + # fake query result to be used by the transformer for determining + # the neighbor + update_vals = {} + query_result = [{VProps.VITRAGE_TYPE: NOVA_HOST_DATASOURCE, + VProps.ID: hostname}] + + update_vals[KProps.HOST] = hostname + update_vals[KProps.RESOURCE_TYPE] = NOVA_HOST_DATASOURCE + update_vals[KProps.RESOURCE_NAME] = hostname + update_vals[TransformerBase.QUERY_RESULT] = query_result + + return self._generate_event(update_vals) + + def _generate_event_on_instance(self, hostname, instance_id): + # fake query result to be used by the transformer for determining + # the neighbor + update_vals = {} + query_result = [{VProps.VITRAGE_TYPE: NOVA_INSTANCE_DATASOURCE, + VProps.ID: instance_id}] + + update_vals[KProps.HOST] = hostname + update_vals[KProps.RESOURCE_TYPE] = NOVA_INSTANCE_DATASOURCE + update_vals[KProps.RESOURCE_NAME] = hostname + update_vals[TransformerBase.QUERY_RESULT] = query_result + + return self._generate_event(update_vals) + + def _is_erroneous(self, vertex): + return vertex[VProps.SEVERITY] != KState.OK