From be6fb21e19f2208cb984e7d5584bbba2ee8a2b7f Mon Sep 17 00:00:00 2001 From: haali1 Date: Mon, 27 Jun 2016 15:09:31 -0700 Subject: [PATCH] Add HipChat and Slack Notification types This patch 1. Supports for loading new notification types as plugins. 2) Adds new plugins for HipChat and Slack 3) Insert Notification types during startup Partially-implements: blueprint notification-engine-plugin Change-Id: I246ced3fe22a9797a3c8384f7bda166797cfac3a --- .../common/repositories/base/base_repo.py | 4 +- .../common/repositories/mysql/mysql_repo.py | 30 ++++- .../common/repositories/orm/models.py | 7 +- .../common/repositories/orm/orm_repo.py | 27 +++- .../repositories/postgres/pgsql_repo.py | 28 ++++- monasca_notification/notification_engine.py | 4 +- monasca_notification/periodic_engine.py | 4 +- monasca_notification/plugins/__init__.py | 0 .../{types => plugins}/abstract_notifier.py | 2 +- .../{types => plugins}/email_notifier.py | 4 +- .../plugins/hipchat_notifier.py | 112 +++++++++++++++++ .../{types => plugins}/pagerduty_notifier.py | 5 +- .../plugins/slack_notifier.py | 116 ++++++++++++++++++ .../{types => plugins}/webhook_notifier.py | 4 +- .../processors/notification_processor.py | 22 +++- monasca_notification/retry_engine.py | 4 +- monasca_notification/types/notifiers.py | 19 ++- notification.yaml | 14 +++ tests/test_email_notification.py | 16 +-- tests/test_notification_processor.py | 15 ++- tests/test_notifiers.py | 31 ++++- tests/test_pagerduty_notification.py | 6 +- tests/test_webhook_notification.py | 6 +- 23 files changed, 435 insertions(+), 45 deletions(-) create mode 100644 monasca_notification/plugins/__init__.py rename monasca_notification/{types => plugins}/abstract_notifier.py (93%) rename monasca_notification/{types => plugins}/email_notifier.py (99%) create mode 100644 monasca_notification/plugins/hipchat_notifier.py rename monasca_notification/{types => plugins}/pagerduty_notifier.py (94%) create mode 100644 monasca_notification/plugins/slack_notifier.py rename monasca_notification/{types => plugins}/webhook_notifier.py (94%) diff --git a/monasca_notification/common/repositories/base/base_repo.py b/monasca_notification/common/repositories/base/base_repo.py index 60b5377..3cddc48 100644 --- a/monasca_notification/common/repositories/base/base_repo.py +++ b/monasca_notification/common/repositories/base/base_repo.py @@ -1,5 +1,5 @@ # Copyright 2015 FUJITSU LIMITED -# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP # # 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 @@ -21,3 +21,5 @@ class BaseRepo(object): self._find_alarm_state_sql = """SELECT state FROM alarm WHERE alarm.id = %s""" + self._insert_notification_types_sql = """INSERT INTO notification_method_type (name) VALUES ( %s)""" + self._find_all_notification_types_sql = """SELECT name from notification_method_type """ diff --git a/monasca_notification/common/repositories/mysql/mysql_repo.py b/monasca_notification/common/repositories/mysql/mysql_repo.py index 6527ffe..d9e857e 100644 --- a/monasca_notification/common/repositories/mysql/mysql_repo.py +++ b/monasca_notification/common/repositories/mysql/mysql_repo.py @@ -1,5 +1,5 @@ # Copyright 2015 FUJITSU LIMITED -# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP # # 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 @@ -85,4 +85,30 @@ class MysqlRepo(BaseRepo): except pymysql.Error as e: self._mysql = None log.exception("Couldn't fetch the current alarm state %s", e) - raise exc.DatabaseException(e) \ No newline at end of file + raise exc.DatabaseException(e) + + def fetch_notification_method_types(self): + try: + if self._mysql is None: + self._connect_to_mysql() + cur = self._mysql.cursor() + cur.execute(self._find_all_notification_types_sql) + + for row in cur: + yield (row[0]) + except pymysql.Error as e: + self._mysql = None + log.exception("Couldn't fetch notification types %s", e) + raise exc.DatabaseException(e) + + def insert_notification_method_types(self, notification_types): + try: + if self._mysql is None: + self._connect_to_mysql() + cur = self._mysql.cursor() + cur.executemany(self._insert_notification_types_sql, notification_types) + + except pymysql.Error as e: + self._mysql = None + log.exception("Couldn't insert notification types %s", e) + raise exc.DatabaseException(e) \ No newline at end of file diff --git a/monasca_notification/common/repositories/orm/models.py b/monasca_notification/common/repositories/orm/models.py index 37e3152..95d59d6 100644 --- a/monasca_notification/common/repositories/orm/models.py +++ b/monasca_notification/common/repositories/orm/models.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Copyright 2015 Fujitsu Technology Solutions -# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -44,3 +44,8 @@ def create_notification_method_model(metadata=None): Column('period', int), Column('created_at', DateTime, default=lambda: datetime.utcnow()), Column('updated_at', DateTime, onupdate=lambda: datetime.utcnow())) + + +def create_notification_method_type_model(metadata=None): + return Table('notification_method_type', metadata, + Column('name', String(20), primary_key=True)) diff --git a/monasca_notification/common/repositories/orm/orm_repo.py b/monasca_notification/common/repositories/orm/orm_repo.py index af5bbe7..e00dec5 100644 --- a/monasca_notification/common/repositories/orm/orm_repo.py +++ b/monasca_notification/common/repositories/orm/orm_repo.py @@ -1,5 +1,5 @@ # Copyright 2015 FUJITSU LIMITED -# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP # # 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 @@ -30,6 +30,7 @@ class OrmRepo(object): aa = models.create_alarm_action_model(metadata).alias('aa') nm = models.create_notification_method_model(metadata).alias('nm') + nmt = models.create_notification_method_type_model(metadata).alias('nmt') self._orm_query = select([nm.c.name, nm.c.type, nm.c.address, nm.c.periodic_interval])\ .select_from(aa.join(nm, aa.c.action_id == nm.c.id))\ @@ -37,6 +38,8 @@ class OrmRepo(object): and_(aa.c.alarm_definition_id == bindparam('alarm_definition_id'), aa.c.alarm_state == bindparam('alarm_state'))) + self._orm_nmt_query = select([nmt.c.name]) + self._orm = None def fetch_notification(self, alarm): @@ -51,3 +54,25 @@ class OrmRepo(object): except DatabaseError as e: log.exception("Couldn't fetch alarms actions %s", e) raise exc.DatabaseException(e) + + def fetch_notification_method_types(self): + try: + with self._orm_engine.connect() as conn: + log.debug('Orm query {%s}', str(self._orm_nmt_query)) + notification_method_types = conn.execute(self._orm_nmt_query).fetchall() + + return [row[0] for row in notification_method_types] + except DatabaseError as e: + log.exception("Couldn't fetch notification method types %s", e) + raise exc.DatabaseException(e) + + def insert_notification_method_types(self, notification_types): + try: + with self._orm_engine.connect() as conn: + for notification_type in notification_types: + conn.execute(self.nmt.insert(), notification_type) + + except DatabaseError as e: + self._mysql = None + log.exception("Couldn't insert notification types %s", e) + raise exc.DatabaseException(e) diff --git a/monasca_notification/common/repositories/postgres/pgsql_repo.py b/monasca_notification/common/repositories/postgres/pgsql_repo.py index 2bafd1f..c1f9bd1 100644 --- a/monasca_notification/common/repositories/postgres/pgsql_repo.py +++ b/monasca_notification/common/repositories/postgres/pgsql_repo.py @@ -1,5 +1,5 @@ # Copyright 2015 FUJITSU LIMITED -# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP # # 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 @@ -59,3 +59,29 @@ class PostgresqlRepo(BaseRepo): except psycopg2.Error as e: log.exception("Couldn't fetch current alarm state %s", e) raise exc.DatabaseException(e) + + def fetch_notification_method_types(self): + try: + if self._pgsql is None: + self._connect_to_pgsql() + cur = self._pgsql.cursor() + cur.execute(self._find_all_notification_types_sql) + + for row in cur: + yield (row[0]) + except psycopg2.Error as e: + self._mysql = None + log.exception("Couldn't fetch notification types %s", e) + raise exc.DatabaseException(e) + + def insert_notification_method_types(self, notification_types): + try: + if self._pgsql is None: + self._connect_to_pgsql() + cur = self._pgsql.cursor() + cur.executemany(self._insert_notification_types_sql, notification_types) + + except psycopg2.Error as e: + self._mysql = None + log.exception("Couldn't insert notification types %s", e) + raise exc.DatabaseException(e) diff --git a/monasca_notification/notification_engine.py b/monasca_notification/notification_engine.py index 1816070..7609b79 100644 --- a/monasca_notification/notification_engine.py +++ b/monasca_notification/notification_engine.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ class NotificationEngine(object): self._producer = KafkaProducer(config['kafka']['url']) self._alarm_ttl = config['processors']['alarm']['ttl'] self._alarms = AlarmProcessor(self._alarm_ttl, config) - self._notifier = NotificationProcessor(config['notification_types']) + self._notifier = NotificationProcessor(config) self._config = config diff --git a/monasca_notification/periodic_engine.py b/monasca_notification/periodic_engine.py index aab1236..db8d3bb 100644 --- a/monasca_notification/periodic_engine.py +++ b/monasca_notification/periodic_engine.py @@ -1,4 +1,4 @@ -# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -45,7 +45,7 @@ class PeriodicEngine(object): self._producer = KafkaProducer(config['kafka']['url']) - self._notifier = NotificationProcessor(config['notification_types']) + self._notifier = NotificationProcessor(config) self._db_repo = get_db_repo(config) def _keep_sending(self, alarm_id, original_state): diff --git a/monasca_notification/plugins/__init__.py b/monasca_notification/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/monasca_notification/types/abstract_notifier.py b/monasca_notification/plugins/abstract_notifier.py similarity index 93% rename from monasca_notification/types/abstract_notifier.py rename to monasca_notification/plugins/abstract_notifier.py index e03d6ac..5426725 100644 --- a/monasca_notification/types/abstract_notifier.py +++ b/monasca_notification/plugins/abstract_notifier.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/monasca_notification/types/email_notifier.py b/monasca_notification/plugins/email_notifier.py similarity index 99% rename from monasca_notification/types/email_notifier.py rename to monasca_notification/plugins/email_notifier.py index c7a50c2..266ef49 100644 --- a/monasca_notification/types/email_notifier.py +++ b/monasca_notification/plugins/email_notifier.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import email.mime.text import smtplib import time -from abstract_notifier import AbstractNotifier +from monasca_notification.plugins.abstract_notifier import AbstractNotifier EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message} diff --git a/monasca_notification/plugins/hipchat_notifier.py b/monasca_notification/plugins/hipchat_notifier.py new file mode 100644 index 0000000..111a9c1 --- /dev/null +++ b/monasca_notification/plugins/hipchat_notifier.py @@ -0,0 +1,112 @@ +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP +# +# 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 json +import requests +import urlparse + +from monasca_notification.plugins.abstract_notifier import AbstractNotifier + +""" + notification.address = https://hipchat.hpcloud.net/v2/room//notification?auth_token=432432 + + How to get access token? + 1) Login to Hipchat with the user account which is used for notification + 2) Go to this page. https://hipchat.hpcloud.net/account/api (Replace your hipchat server name) + 3) You can see option to "Create token". Use the capability "SendNotification" + + How to get the Room ID? + 1) Login to Hipchat with the user account which is used for notification + 2) Go to this page. https://hipchat.hpcloud.net/account/api (Replace your hipchat server name) + 3) Click on the Rooms tab + 4) Click on any Room of your choice. + 5) Room ID is the API ID field + +""" + + +class HipChatNotifier(AbstractNotifier): + def __init__(self, log): + self._log = log + + def config(self, config_dict): + self._config = {'timeout': 5} + self._config.update(config_dict) + + @property + def type(self): + return "hipchat" + + @property + def statsd_name(self): + return 'sent_hipchat_count' + + def _build_hipchat_message(self, notification): + """Builds hipchat message body + """ + body = {'alarm_id': notification.alarm_id, + 'alarm_definition_id': notification.raw_alarm['alarmDefinitionId'], + 'alarm_name': notification.alarm_name, + 'alarm_description': notification.raw_alarm['alarmDescription'], + 'alarm_timestamp': notification.alarm_timestamp, + 'state': notification.state, + 'old_state': notification.raw_alarm['oldState'], + 'message': notification.message, + 'tenant_id': notification.tenant_id, + 'metrics': notification.metrics} + + hipchat_request = {} + hipchat_request['color'] = 'green' + hipchat_request['message_format'] = 'text' + hipchat_request['message'] = json.dumps(body, indent=3) + + return hipchat_request + + def send_notification(self, notification): + """Send the notification via hipchat + Posts on the given url + """ + + hipchat_message = self._build_hipchat_message(notification) + parsed_url = urlparse.urlsplit(notification.address) + + query_params = urlparse.parse_qs(parsed_url.query) + # URL without query params + url = urlparse.urljoin(notification.address, urlparse.urlparse(notification.address).path) + + # Default option is to do cert verification + verify = self._config.get('insecure', False) + # If ca_certs is specified, do cert validation and ignore insecure flag + if (self._config.get("ca_certs")): + verify = self._config.get("ca_certs") + + try: + # Posting on the given URL + result = requests.post(url=url, + data=hipchat_message, + verify=verify, + params=query_params, + timeout=self._config['timeout']) + + if result.status_code in range(200, 300): + self._log.info("Notification successfully posted.") + return True + else: + msg = "Received an HTTP code {} when trying to send to hipchat on URL {} with response {} ." + self._log.error(msg.format(result.status_code, url, result.text)) + return False + except Exception: + self._log.exception("Error trying to send to hipchat on URL {}".format(url)) + return False diff --git a/monasca_notification/types/pagerduty_notifier.py b/monasca_notification/plugins/pagerduty_notifier.py similarity index 94% rename from monasca_notification/types/pagerduty_notifier.py rename to monasca_notification/plugins/pagerduty_notifier.py index f130aa1..04b6f95 100644 --- a/monasca_notification/types/pagerduty_notifier.py +++ b/monasca_notification/plugins/pagerduty_notifier.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,8 @@ import json import requests -from abstract_notifier import AbstractNotifier +from monasca_notification.plugins.abstract_notifier import AbstractNotifier + VALID_HTTP_CODES = [200, 201, 204] diff --git a/monasca_notification/plugins/slack_notifier.py b/monasca_notification/plugins/slack_notifier.py new file mode 100644 index 0000000..1d11adb --- /dev/null +++ b/monasca_notification/plugins/slack_notifier.py @@ -0,0 +1,116 @@ +# (C) Copyright 2016 Hewlett Packard Enterprise Development LP +# +# 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 json +import requests +import urlparse + +from monasca_notification.plugins.abstract_notifier import AbstractNotifier + +""" + notification.address = https://slack.com/api/chat.postMessage?token=token&channel=#channel" + + Slack documentation about tokens: + 1. Login to your slack account via browser and check the following pages + a. https://api.slack.com/docs/oauth-test-tokens + b. https://api.slack.com/tokens + +""" + + +class SlackNotifier(AbstractNotifier): + def __init__(self, log): + self._log = log + + def config(self, config_dict): + self._config = {'timeout': 5} + self._config.update(config_dict) + + @property + def type(self): + return "slack" + + @property + def statsd_name(self): + return 'sent_slack_count' + + def _build_slack_message(self, notification): + """Builds slack message body + """ + body = {'alarm_id': notification.alarm_id, + 'alarm_definition_id': notification.raw_alarm['alarmDefinitionId'], + 'alarm_name': notification.alarm_name, + 'alarm_description': notification.raw_alarm['alarmDescription'], + 'alarm_timestamp': notification.alarm_timestamp, + 'state': notification.state, + 'old_state': notification.raw_alarm['oldState'], + 'message': notification.message, + 'tenant_id': notification.tenant_id, + 'metrics': notification.metrics} + + slack_request = {} + slack_request['text'] = json.dumps(body, indent=3) + + return slack_request + + def send_notification(self, notification): + """Send the notification via slack + Posts on the given url + """ + + slack_message = self._build_slack_message(notification) + + address = notification.address + # "#" is reserved character and replace it with ascii equivalent + # Slack room has "#" as first character + address = address.replace("#", "%23") + + parsed_url = urlparse.urlsplit(address) + query_params = urlparse.parse_qs(parsed_url.query) + # URL without query params + url = urlparse.urljoin(address, urlparse.urlparse(address).path) + + # Default option is to do cert verification + verify = self._config.get('insecure', False) + # If ca_certs is specified, do cert validation and ignore insecure flag + if (self._config.get("ca_certs")): + verify = self._config.get("ca_certs") + + try: + # Posting on the given URL + self._log.debug("Sending to the url {0} , with query_params {1}".format(url, query_params)) + result = requests.post(url=url, + data=slack_message, + verify=verify, + params=query_params, + timeout=self._config['timeout']) + + if result.status_code not in range(200, 300): + self._log.error("Received an HTTP code {} when trying to post on URL {}." + .format(result.status_code, url)) + return False + + # Slack returns 200 ok even if the token is invalid. Response has valid error message + response = json.loads(result.text) + if response.get('ok'): + self._log.info("Notification successfully posted.") + return True + else: + self._log.error("Received an error message {} when trying to send to slack on URL {}." + .format(response.get("error"), url)) + return False + except Exception: + self._log.exception("Error trying to send to slack on URL {}".format(url)) + return False diff --git a/monasca_notification/types/webhook_notifier.py b/monasca_notification/plugins/webhook_notifier.py similarity index 94% rename from monasca_notification/types/webhook_notifier.py rename to monasca_notification/plugins/webhook_notifier.py index c742292..98982b0 100644 --- a/monasca_notification/types/webhook_notifier.py +++ b/monasca_notification/plugins/webhook_notifier.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ import json import requests -from abstract_notifier import AbstractNotifier +from monasca_notification.plugins.abstract_notifier import AbstractNotifier class WebhookNotifier(AbstractNotifier): diff --git a/monasca_notification/processors/notification_processor.py b/monasca_notification/processors/notification_processor.py index bfc641c..76554cf 100644 --- a/monasca_notification/processors/notification_processor.py +++ b/monasca_notification/processors/notification_processor.py @@ -1,4 +1,4 @@ -# (C) Copyright 2014-2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import logging import monascastatsd +from monasca_notification.common.utils import get_db_repo from monasca_notification.processors.base import BaseProcessor from monasca_notification.types import notifiers @@ -27,7 +28,24 @@ class NotificationProcessor(BaseProcessor): def __init__(self, config): self.statsd = monascastatsd.Client(name='monasca', dimensions=BaseProcessor.dimensions) notifiers.init(self.statsd) - notifiers.config(config) + notifiers.load_plugins(config['notification_types']) + notifiers.config(config['notification_types']) + self._db_repo = get_db_repo(config) + self.insert_configured_plugins() + + def insert_configured_plugins(self): + """Persists configured plugin types in DB + For each notification type configured add it in db, if it is not there + """ + configured_plugin_types = notifiers.enabled_notifications() + + persisted_plugin_types = self._db_repo.fetch_notification_method_types() + remaining_plugin_types = set(configured_plugin_types) - set(persisted_plugin_types) + + if remaining_plugin_types: + log.info("New plugins detected: Adding new notification types {} to database" + .format(remaining_plugin_types)) + self._db_repo.insert_notification_method_types(remaining_plugin_types) def send(self, notifications): """Send the notifications diff --git a/monasca_notification/retry_engine.py b/monasca_notification/retry_engine.py index 3cd80f2..5417a1a 100644 --- a/monasca_notification/retry_engine.py +++ b/monasca_notification/retry_engine.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,7 +47,7 @@ class RetryEngine(object): self._producer = KafkaProducer(config['kafka']['url']) - self._notifier = NotificationProcessor(config['notification_types']) + self._notifier = NotificationProcessor(config) def run(self): for raw_notification in self._consumer: diff --git a/monasca_notification/types/notifiers.py b/monasca_notification/types/notifiers.py index ed95b1d..ff2be98 100644 --- a/monasca_notification/types/notifiers.py +++ b/monasca_notification/types/notifiers.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,9 +16,10 @@ import logging import time -from monasca_notification.types import email_notifier -from monasca_notification.types import pagerduty_notifier -from monasca_notification.types import webhook_notifier +from monasca_common.simport import simport +from monasca_notification.plugins import email_notifier +from monasca_notification.plugins import pagerduty_notifier +from monasca_notification.plugins import webhook_notifier log = logging.getLogger(__name__) @@ -40,10 +41,18 @@ def init(statsd_obj): possible_notifiers.append(pagerduty_notifier.PagerdutyNotifier(log)) +def load_plugins(config): + for plugin_class in config.get("plugins", []): + try: + possible_notifiers.append(simport.load(plugin_class)(log)) + except Exception: + log.exception("unable to load the class {0} , ignoring it".format(plugin_class)) + + def enabled_notifications(): results = [] for key in configured_notifiers: - results.append(key) + results.append(key.upper()) return results diff --git a/notification.yaml b/notification.yaml index 32330c8..40a029d 100644 --- a/notification.yaml +++ b/notification.yaml @@ -33,6 +33,10 @@ postgresql: host: 127.0.0.1 notification_types: + plugins: + - monasca_notification.plugins.hipchat_notifier:HipChatNotifier + - monasca_notification.plugins.slack_notifier:SlackNotifier + email: server: 192.168.10.4 port: 25 @@ -48,6 +52,16 @@ notification_types: timeout: 5 url: "https://events.pagerduty.com/generic/2010-04-15/create_event.json" + hipchat: + timeout: 5 + ca_certs: "/etc/ssl/certs/ca-certificates.crt" + insecure: False + + slack: + timeout: 5 + ca_certs: "/etc/ssl/certs/ca-certificates.crt" + insecure: False + processors: alarm: number: 2 diff --git a/tests/test_email_notification.py b/tests/test_email_notification.py index 763c9be..9b84052 100644 --- a/tests/test_email_notification.py +++ b/tests/test_email_notification.py @@ -1,4 +1,4 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import time import unittest from monasca_notification.notification import Notification -from monasca_notification.types import email_notifier +from monasca_notification.plugins import email_notifier UNICODE_CHAR = unichr(2344) UNICODE_CHAR_ENCODED = UNICODE_CHAR.encode("utf-8") @@ -90,7 +90,7 @@ class TestEmail(unittest.TestCase): def _smtbStubException(self, *arg, **kwargs): return smtpStubException(self.trap) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def notify(self, smtp_stub, metric, mock_smtp): mock_smtp.SMTP = smtp_stub @@ -185,7 +185,7 @@ class TestEmail(unittest.TestCase): return_value = self.trap.pop(0) self.assertTrue(return_value) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def test_smtp_sendmail_failed_connection_twice(self, mock_smtp): """Email that fails on smtp_connect twice """ @@ -229,7 +229,7 @@ class TestEmail(unittest.TestCase): self.assertIn("SMTP server disconnected. Will reconnect and retry message.", self.trap) self.assertIn("Unable to connect to email server.", self.trap) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def test_smtp_sendmail_smtp_None(self, mock_smtp): """Email that fails on smtp_connect twice """ @@ -277,7 +277,7 @@ class TestEmail(unittest.TestCase): .format(self.email_config['server']), self.trap) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def test_smtp_sendmail_failed_connection_once_then_email(self, mock_smtp): """Email that fails on smtp_connect once then email """ @@ -319,7 +319,7 @@ class TestEmail(unittest.TestCase): self.assertIn("Error sending Email Notification", self.trap) self.assertNotIn("Unable to connect to email server.", self.trap) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def test_smtp_sendmail_failed_connection_once(self, mock_smtp): """Email that fails on smtp_connect once """ @@ -359,7 +359,7 @@ class TestEmail(unittest.TestCase): self.assertIn("Sent email to %s, notification %s" % (notification.address, notification.to_json()), self.trap) - @mock.patch('monasca_notification.types.email_notifier.smtplib') + @mock.patch('monasca_notification.plugins.email_notifier.smtplib') def test_smtp_sendmail_failed_exception(self, mock_smtp): """Email that fails on exception """ diff --git a/tests/test_notification_processor.py b/tests/test_notification_processor.py index b76e0f4..d506c07 100644 --- a/tests/test_notification_processor.py +++ b/tests/test_notification_processor.py @@ -1,4 +1,4 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -47,17 +47,24 @@ class TestNotificationProcessor(unittest.TestCase): 'timeout': 60, 'from_addr': 'hpcs.mon@hp.com'} + self.mysql_config = {'ssl': None, + 'host': 'mysql_host', + 'port': 'mysql_port', + 'user': 'mysql_user', + 'db': 'dbname', + 'passwd': 'mysql_passwd'} + def tearDown(self): pass # ------------------------------------------------------------------------ # Test helper functions # ------------------------------------------------------------------------ - + @mock.patch('pymysql.connect') @mock.patch('monasca_notification.processors.notification_processor.monascastatsd') @mock.patch('monasca_notification.types.notifiers.email_notifier.smtplib') @mock.patch('monasca_notification.processors.notification_processor.notifiers.log') - def _start_processor(self, notifications, mock_log, mock_smtp, mock_statsd): + def _start_processor(self, notifications, mock_log, mock_smtp, mock_statsd, mock_pymsql): """Start the processor with the proper mocks """ # Since the log runs in another thread I can mock it directly, instead change the methods to put to a queue @@ -68,6 +75,8 @@ class TestNotificationProcessor(unittest.TestCase): config = {} config["email"] = self.email_config + config["mysql"] = self.mysql_config + config["notification_types"] = {} processor = (notification_processor.NotificationProcessor(config)) processor.send(notifications) diff --git a/tests/test_notifiers.py b/tests/test_notifiers.py index fceedb1..5891f3c 100644 --- a/tests/test_notifiers.py +++ b/tests/test_notifiers.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -153,7 +153,7 @@ class TestInterface(unittest.TestCase): self.assertEqual(len(notifications), 3) self.assertEqual(sorted(notifications), - ["email", "pagerduty", "webhook"]) + ["EMAIL", "PAGERDUTY", "WEBHOOK"]) @mock.patch('monasca_notification.types.notifiers.email_notifier.smtplib') @mock.patch('monasca_notification.types.notifiers.log') @@ -364,3 +364,30 @@ class TestInterface(unittest.TestCase): self.assertEqual(self.statsd.timer.timer_calls['email_time_start'], 3) self.assertEqual(self.statsd.timer.timer_calls['email_time_stop'], 3) self.assertEqual(self.statsd.counter.counter, 3) + + def test_plugin_load(self): + config_dict = {"plugins": ["monasca_notification.plugins.hipchat_notifier:HipChatNotifier", + "monasca_notification.plugins.slack_notifier:SlackNotifier"]} + + notifiers.init(self.statsd) + notifiers.load_plugins(config_dict) + self.assertEqual(len(notifiers.possible_notifiers), 5) + + configured_plugins = ["email", "webhook", "pagerduty", "hipchat", "slack"] + for plugin in notifiers.configured_notifiers: + self.asssertIn(plugin.type in configured_plugins) + + @mock.patch('monasca_notification.types.notifiers.log') + def test_invalid_plugin_load_exception_ignored(self, mock_log): + mock_log.exception = self.trap.append + config_dict = {"plugins": ["monasca_notification.plugins.hipchat_notifier:UnknownPlugin", + "monasca_notification.plugins.slack_notifier:SlackNotifier"]} + + notifiers.init(self.statsd) + notifiers.load_plugins(config_dict) + self.assertEqual(len(notifiers.possible_notifiers), 4) + self.assertEqual(len(self.trap), 1) + + configured_plugins = ["email", "webhook", "pagerduty", "slack"] + for plugin in notifiers.configured_notifiers: + self.asssertIn(plugin.type in configured_plugins) diff --git a/tests/test_pagerduty_notification.py b/tests/test_pagerduty_notification.py index 8913fe3..afbd7ab 100644 --- a/tests/test_pagerduty_notification.py +++ b/tests/test_pagerduty_notification.py @@ -1,4 +1,4 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import time import unittest from monasca_notification.notification import Notification -from monasca_notification.types import pagerduty_notifier +from monasca_notification.plugins import pagerduty_notifier def alarm(metrics): @@ -125,7 +125,7 @@ class TestWebhook(unittest.TestCase): self.assertRegexpMatches(log_msg, "key=") self.assertRegexpMatches(log_msg, "response=%s" % http_response) - @mock.patch('monasca_notification.types.pagerduty_notifier.requests') + @mock.patch('monasca_notification.plugins.pagerduty_notifier.requests') def notify(self, http_func, mock_requests): mock_log = mock.MagicMock() mock_log.warn = self.trap.put diff --git a/tests/test_webhook_notification.py b/tests/test_webhook_notification.py index 29840ab..8b22f47 100644 --- a/tests/test_webhook_notification.py +++ b/tests/test_webhook_notification.py @@ -1,4 +1,4 @@ -# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import requests import unittest from monasca_notification.notification import Notification -from monasca_notification.types import webhook_notifier +from monasca_notification.plugins import webhook_notifier def alarm(metrics): @@ -67,7 +67,7 @@ class TestWebhook(unittest.TestCase): self.trap.put("timeout %s" % kwargs["timeout"]) raise requests.exceptions.Timeout - @mock.patch('monasca_notification.types.webhook_notifier.requests') + @mock.patch('monasca_notification.plugins.webhook_notifier.requests') def notify(self, http_func, mock_requests): mock_log = mock.MagicMock() mock_log.warn = self.trap.put