summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTomasz Trębski <kornicameister@gmail.com>2017-05-16 00:14:56 +0200
committerTomasz Trębski <tomasz.trebski@ts.fujitsu.com>2017-10-20 09:32:11 +0200
commite1a9b9a96a56ec5476a21556e5ace61996afd74c (patch)
tree866d277d9d74b94f2e6120c52b307d612dd2b057
parent2092e4185c26a0153951906382e7acec4e29fc29 (diff)
Integrate with oslo.conf and oslo.log
Change upgrades the monasca-notification to leverage the capabilities of both oslo.log and oslo.conf: - configuration of logging separated from application settings - ability to enforce data types for application settings - ability to use oslo.config-generator capabilities - automatic configuration parsing done by oslo.cfg That change will bring it closer to the rest of monasca components where such transition has happened already. However, in the rest of monasca, oslo.cfg was partially or fully implemented whereas monasca-notification has been relying on YAML based configuration file. Therefore backward compatybility for such format will be kept for now. Story: 2000959 Task: 4093 Task: 4092 Change-Id: Ia75c3b60d0fada854178f21ca5ccb9e6a880f37f
Notes
Notes (review): Code-Review+1: Adrian Czarnecki <adrian.czarnecki@ts.fujitsu.com> Code-Review+2: Dobroslaw Zybort <dobroslaw.zybort@ts.fujitsu.com> Workflow+1: Dobroslaw Zybort <dobroslaw.zybort@ts.fujitsu.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Tue, 24 Oct 2017 08:04:04 +0000 Reviewed-on: https://review.openstack.org/464768 Project: openstack/monasca-notification Branch: refs/heads/master
-rw-r--r--.gitignore2
-rw-r--r--config-generator/notification.conf5
-rw-r--r--etc/monasca/notification-logging.conf46
-rw-r--r--monasca_notification/common/repositories/mysql/mysql_repo.py18
-rw-r--r--monasca_notification/common/repositories/orm/orm_repo.py32
-rw-r--r--monasca_notification/common/repositories/postgres/pgsql_repo.py4
-rw-r--r--monasca_notification/common/utils.py39
-rw-r--r--monasca_notification/conf/__init__.py162
-rw-r--r--monasca_notification/conf/cli.py40
-rw-r--r--monasca_notification/conf/database.py129
-rw-r--r--monasca_notification/conf/kafka.py77
-rw-r--r--monasca_notification/conf/notifiers.py53
-rw-r--r--monasca_notification/conf/processors.py55
-rw-r--r--monasca_notification/conf/queues.py45
-rw-r--r--monasca_notification/conf/retry.py41
-rw-r--r--monasca_notification/conf/statsd.py41
-rw-r--r--monasca_notification/conf/types.py102
-rw-r--r--monasca_notification/conf/zookeeper.py61
-rw-r--r--monasca_notification/config.py64
-rw-r--r--monasca_notification/main.py79
-rw-r--r--monasca_notification/notification_engine.py45
-rw-r--r--monasca_notification/periodic_engine.py26
-rw-r--r--monasca_notification/plugins/abstract_notifier.py5
-rw-r--r--monasca_notification/plugins/email_notifier.py58
-rw-r--r--monasca_notification/plugins/hipchat_notifier.py50
-rw-r--r--monasca_notification/plugins/jira_notifier.py69
-rw-r--r--monasca_notification/plugins/pagerduty_notifier.py40
-rw-r--r--monasca_notification/plugins/slack_notifier.py52
-rw-r--r--monasca_notification/plugins/webhook_notifier.py34
-rw-r--r--monasca_notification/processors/alarm_processor.py14
-rw-r--r--monasca_notification/processors/notification_processor.py14
-rw-r--r--monasca_notification/retry_engine.py45
-rw-r--r--monasca_notification/types/notifiers.py50
-rw-r--r--monasca_notification/version.py23
-rw-r--r--requirements.txt7
-rw-r--r--setup.cfg4
-rw-r--r--test-requirements.txt14
-rw-r--r--tests/base.py83
-rw-r--r--tests/resources/notification.yaml125
-rw-r--r--tests/test_alarm_processor.py24
-rw-r--r--tests/test_config.py226
-rw-r--r--tests/test_email_notification.py38
-rw-r--r--tests/test_hipchat_notification.py11
-rw-r--r--tests/test_jira_notification.py66
-rw-r--r--tests/test_mysql_repo.py19
-rw-r--r--tests/test_notification_processor.py52
-rw-r--r--tests/test_notifiers.py173
-rw-r--r--tests/test_orm_repo.py11
-rw-r--r--tests/test_pagerduty_notification.py17
-rw-r--r--tests/test_slack_notification.py78
-rw-r--r--tests/test_utils.py26
-rw-r--r--tests/test_webhook_notification.py13
-rw-r--r--tox.ini8
53 files changed, 2007 insertions, 608 deletions
diff --git a/.gitignore b/.gitignore
index b62244d..7475aca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,3 +16,5 @@ dist
16.stestr/ 16.stestr/
17.coverage.* 17.coverage.*
18cover/ 18cover/
19
20*.sample
diff --git a/config-generator/notification.conf b/config-generator/notification.conf
new file mode 100644
index 0000000..14fca52
--- /dev/null
+++ b/config-generator/notification.conf
@@ -0,0 +1,5 @@
1[DEFAULT]
2output_file = etc/monasca/notification.conf.sample
3wrap_width = 80
4namespace = monasca_notification
5namespace = oslo.log
diff --git a/etc/monasca/notification-logging.conf b/etc/monasca/notification-logging.conf
new file mode 100644
index 0000000..5b8814c
--- /dev/null
+++ b/etc/monasca/notification-logging.conf
@@ -0,0 +1,46 @@
1[loggers]
2keys = root, kafka, zookeeper, statsd
3
4[handlers]
5keys = console, file
6
7[formatters]
8keys = context
9
10[logger_root]
11level = DEBUG
12handlers = console, file
13
14[logger_kafka]
15qualname = kafka
16level = DEBUG
17handlers = console, file
18propagate = 0
19
20[logger_zookeeper]
21qualname = zookeeper
22level = DEBUG
23handlers = console, file
24propagate = 0
25
26[logger_statsd]
27qualname = statsd
28level = DEBUG
29handlers = console, file
30propagate = 0
31
32[handler_console]
33class = logging.StreamHandler
34args = (sys.stderr,)
35level = DEBUG
36formatter = context
37
38[handler_file]
39class = logging.handlers.RotatingFileHandler
40level = DEBUG
41formatter = context
42# store up to 5*100MB of logs
43args = ('monasca-notification.log', 'a', 104857600, 5)
44
45[formatter_context]
46class = oslo_log.formatters.ContextFormatter
diff --git a/monasca_notification/common/repositories/mysql/mysql_repo.py b/monasca_notification/common/repositories/mysql/mysql_repo.py
index 2209ee0..eb4a926 100644
--- a/monasca_notification/common/repositories/mysql/mysql_repo.py
+++ b/monasca_notification/common/repositories/mysql/mysql_repo.py
@@ -11,7 +11,7 @@
11# or implied. See the License for the specific language governing permissions and limitations under 11# or implied. See the License for the specific language governing permissions and limitations under
12# the License. 12# the License.
13 13
14import logging 14from oslo_log import log as logging
15import pymysql 15import pymysql
16 16
17from monasca_notification.common.repositories.base import base_repo 17from monasca_notification.common.repositories.base import base_repo
@@ -24,21 +24,9 @@ log = logging.getLogger(__name__)
24class MysqlRepo(base_repo.BaseRepo): 24class MysqlRepo(base_repo.BaseRepo):
25 def __init__(self, config): 25 def __init__(self, config):
26 super(MysqlRepo, self).__init__(config) 26 super(MysqlRepo, self).__init__(config)
27 if 'ssl' in config['mysql']: 27 self._mysql_ssl = config['mysql']['ssl'] or None
28 self._mysql_ssl = config['mysql']['ssl']
29 else:
30 self._mysql_ssl = None
31
32 if 'port' in config['mysql']:
33 self._mysql_port = config['mysql']['port']
34 else:
35 #
36 # If port isn't specified in the config file,
37 # set it to the default value.
38 #
39 self._mysql_port = 3306
40
41 self._mysql_host = config['mysql']['host'] 28 self._mysql_host = config['mysql']['host']
29 self._mysql_port = config['mysql']['port']
42 self._mysql_user = config['mysql']['user'] 30 self._mysql_user = config['mysql']['user']
43 self._mysql_passwd = config['mysql']['passwd'] 31 self._mysql_passwd = config['mysql']['passwd']
44 self._mysql_dbname = config['mysql']['db'] 32 self._mysql_dbname = config['mysql']['db']
diff --git a/monasca_notification/common/repositories/orm/orm_repo.py b/monasca_notification/common/repositories/orm/orm_repo.py
index 508e7f1..cd76d21 100644
--- a/monasca_notification/common/repositories/orm/orm_repo.py
+++ b/monasca_notification/common/repositories/orm/orm_repo.py
@@ -1,4 +1,4 @@
1# Copyright 2015 FUJITSU LIMITED 1# Copyright 2015-2017 FUJITSU LIMITED
2# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP 2# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP
3# 3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
@@ -11,7 +11,8 @@
11# or implied. See the License for the specific language governing permissions and limitations under 11# or implied. See the License for the specific language governing permissions and limitations under
12# the License. 12# the License.
13 13
14import logging 14from oslo_config import cfg
15from oslo_log import log as logging
15from sqlalchemy import engine_from_config, MetaData 16from sqlalchemy import engine_from_config, MetaData
16from sqlalchemy.sql import select, bindparam, and_, insert 17from sqlalchemy.sql import select, bindparam, and_, insert
17from sqlalchemy.exc import DatabaseError 18from sqlalchemy.exc import DatabaseError
@@ -19,12 +20,15 @@ from sqlalchemy.exc import DatabaseError
19from monasca_notification.common.repositories import exceptions as exc 20from monasca_notification.common.repositories import exceptions as exc
20from monasca_notification.common.repositories.orm import models 21from monasca_notification.common.repositories.orm import models
21 22
22log = logging.getLogger(__name__) 23LOG = logging.getLogger(__name__)
24CONF = cfg.CONF
23 25
24 26
25class OrmRepo(object): 27class OrmRepo(object):
26 def __init__(self, config): 28 def __init__(self):
27 self._orm_engine = engine_from_config(config['database']['orm'], prefix='') 29 self._orm_engine = engine_from_config({
30 'url': CONF.orm.url
31 }, prefix='')
28 32
29 metadata = MetaData() 33 metadata = MetaData()
30 34
@@ -54,38 +58,38 @@ class OrmRepo(object):
54 def fetch_notifications(self, alarm): 58 def fetch_notifications(self, alarm):
55 try: 59 try:
56 with self._orm_engine.connect() as conn: 60 with self._orm_engine.connect() as conn:
57 log.debug('Orm query {%s}', str(self._orm_query)) 61 LOG.debug('Orm query {%s}', str(self._orm_query))
58 notifications = conn.execute(self._orm_query, 62 notifications = conn.execute(self._orm_query,
59 alarm_definition_id=alarm['alarmDefinitionId'], 63 alarm_definition_id=alarm['alarmDefinitionId'],
60 alarm_state=alarm['newState']) 64 alarm_state=alarm['newState'])
61 65
62 return [(row[0], row[1].lower(), row[2], row[3], row[4]) for row in notifications] 66 return [(row[0], row[1].lower(), row[2], row[3], row[4]) for row in notifications]
63 except DatabaseError as e: 67 except DatabaseError as e:
64 log.exception("Couldn't fetch alarms actions %s", e) 68 LOG.exception("Couldn't fetch alarms actions %s", e)
65 raise exc.DatabaseException(e) 69 raise exc.DatabaseException(e)
66 70
67 def get_alarm_current_state(self, alarm_id): 71 def get_alarm_current_state(self, alarm_id):
68 try: 72 try:
69 with self._orm_engine.connect() as conn: 73 with self._orm_engine.connect() as conn:
70 log.debug('Orm query {%s}', str(self._orm_get_alarm_state)) 74 LOG.debug('Orm query {%s}', str(self._orm_get_alarm_state))
71 result = conn.execute(self._orm_get_alarm_state, 75 result = conn.execute(self._orm_get_alarm_state,
72 alarm_id=alarm_id) 76 alarm_id=alarm_id)
73 row = result.fetchone() 77 row = result.fetchone()
74 state = row[0] if row is not None else None 78 state = row[0] if row is not None else None
75 return state 79 return state
76 except DatabaseError as e: 80 except DatabaseError as e:
77 log.exception("Couldn't fetch the current alarm state %s", e) 81 LOG.exception("Couldn't fetch the current alarm state %s", e)
78 raise exc.DatabaseException(e) 82 raise exc.DatabaseException(e)
79 83
80 def fetch_notification_method_types(self): 84 def fetch_notification_method_types(self):
81 try: 85 try:
82 with self._orm_engine.connect() as conn: 86 with self._orm_engine.connect() as conn:
83 log.debug('Orm query {%s}', str(self._orm_nmt_query)) 87 LOG.debug('Orm query {%s}', str(self._orm_nmt_query))
84 notification_method_types = conn.execute(self._orm_nmt_query).fetchall() 88 notification_method_types = conn.execute(self._orm_nmt_query).fetchall()
85 89
86 return [row[0] for row in notification_method_types] 90 return [row[0] for row in notification_method_types]
87 except DatabaseError as e: 91 except DatabaseError as e:
88 log.exception("Couldn't fetch notification method types %s", e) 92 LOG.exception("Couldn't fetch notification method types %s", e)
89 raise exc.DatabaseException(e) 93 raise exc.DatabaseException(e)
90 94
91 def insert_notification_method_types(self, notification_types): 95 def insert_notification_method_types(self, notification_types):
@@ -98,13 +102,13 @@ class OrmRepo(object):
98 conn.execute(self._orm_add_notification_type, b_name=notification_type) 102 conn.execute(self._orm_add_notification_type, b_name=notification_type)
99 103
100 except DatabaseError as e: 104 except DatabaseError as e:
101 log.debug("Failed to insert notification types %s", notification_types) 105 LOG.debug("Failed to insert notification types %s", notification_types)
102 raise exc.DatabaseException(e) 106 raise exc.DatabaseException(e)
103 107
104 def get_notification(self, notification_id): 108 def get_notification(self, notification_id):
105 try: 109 try:
106 with self._orm_engine.connect() as conn: 110 with self._orm_engine.connect() as conn:
107 log.debug('Orm query {%s}', str(self._orm_get_notification)) 111 LOG.debug('Orm query {%s}', str(self._orm_get_notification))
108 result = conn.execute(self._orm_get_notification, 112 result = conn.execute(self._orm_get_notification,
109 notification_id=notification_id) 113 notification_id=notification_id)
110 notification = result.fetchone() 114 notification = result.fetchone()
@@ -113,5 +117,5 @@ class OrmRepo(object):
113 else: 117 else:
114 return [notification[0], notification[1].lower(), notification[2], notification[3]] 118 return [notification[0], notification[1].lower(), notification[2], notification[3]]
115 except DatabaseError as e: 119 except DatabaseError as e:
116 log.exception("Couldn't fetch the notification method %s", e) 120 LOG.exception("Couldn't fetch the notification method %s", e)
117 raise exc.DatabaseException(e) 121 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 95235a6..1ba1e7c 100644
--- a/monasca_notification/common/repositories/postgres/pgsql_repo.py
+++ b/monasca_notification/common/repositories/postgres/pgsql_repo.py
@@ -1,4 +1,4 @@
1# Copyright 2015 FUJITSU LIMITED 1# Copyright 2015-2017 FUJITSU LIMITED
2# (C) Copyright 2016 Hewlett Packard Enterprise Development LP 2# (C) Copyright 2016 Hewlett Packard Enterprise Development LP
3# 3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 4# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
@@ -11,7 +11,7 @@
11# or implied. See the License for the specific language governing permissions and limitations under 11# or implied. See the License for the specific language governing permissions and limitations under
12# the License. 12# the License.
13 13
14import logging 14from oslo_log import log as logging
15import psycopg2 15import psycopg2
16 16
17from monasca_notification.common.repositories.base import base_repo 17from monasca_notification.common.repositories.base import base_repo
diff --git a/monasca_notification/common/utils.py b/monasca_notification/common/utils.py
index 712e265..58c1984 100644
--- a/monasca_notification/common/utils.py
+++ b/monasca_notification/common/utils.py
@@ -13,25 +13,25 @@
13# implied. 13# implied.
14# See the License for the specific language governing permissions and 14# See the License for the specific language governing permissions and
15# limitations under the License. 15# limitations under the License.
16import logging
17import monascastatsd 16import monascastatsd
18 17
19from monasca_common.simport import simport 18from oslo_config import cfg
19from oslo_log import log
20 20
21from monasca_notification.common.repositories import exceptions 21from monasca_notification.common.repositories import exceptions
22from monasca_notification.notification import Notification 22from monasca_notification.notification import Notification
23 23
24log = logging.getLogger(__name__) 24LOG = log.getLogger(__name__)
25CONF = cfg.CONF
25 26
26NOTIFICATION_DIMENSIONS = {'service': 'monitoring', 27NOTIFICATION_DIMENSIONS = {'service': 'monitoring',
27 'component': 'monasca-notification'} 28 'component': 'monasca-notification'}
28 29
29 30
30def get_db_repo(config): 31def get_db_repo():
31 if 'database' in config and 'repo_driver' in config['database']: 32 repo_driver = CONF.database.repo_driver
32 return simport.load(config['database']['repo_driver'])(config) 33 LOG.debug('Enabling the %s RDB repository', repo_driver)
33 else: 34 return repo_driver(CONF)
34 return simport.load('monasca_notification.common.repositories.mysql.mysql_repo:MysqlRepo')(config)
35 35
36 36
37def construct_notification_object(db_repo, notification_json): 37def construct_notification_object(db_repo, notification_json):
@@ -47,7 +47,7 @@ def construct_notification_object(db_repo, notification_json):
47 stored_notification = grab_stored_notification_method(db_repo, notification.id) 47 stored_notification = grab_stored_notification_method(db_repo, notification.id)
48 # Notification method was deleted 48 # Notification method was deleted
49 if stored_notification is None: 49 if stored_notification is None:
50 log.debug("Notification method {0} was deleted from database. " 50 LOG.debug("Notification method {0} was deleted from database. "
51 "Will stop sending.".format(notification.id)) 51 "Will stop sending.".format(notification.id))
52 return None 52 return None
53 # Update notification method with most up to date values 53 # Update notification method with most up to date values
@@ -58,11 +58,11 @@ def construct_notification_object(db_repo, notification_json):
58 notification.period = stored_notification[3] 58 notification.period = stored_notification[3]
59 return notification 59 return notification
60 except exceptions.DatabaseException: 60 except exceptions.DatabaseException:
61 log.warn("Error querying mysql for notification method. " 61 LOG.warn("Error querying mysql for notification method. "
62 "Using currently cached method.") 62 "Using currently cached method.")
63 return notification 63 return notification
64 except Exception as e: 64 except Exception as e:
65 log.warn("Error when attempting to construct notification {0}".format(e)) 65 LOG.warn("Error when attempting to construct notification {0}".format(e))
66 return None 66 return None
67 67
68 68
@@ -70,21 +70,18 @@ def grab_stored_notification_method(db_repo, notification_id):
70 try: 70 try:
71 stored_notification = db_repo.get_notification(notification_id) 71 stored_notification = db_repo.get_notification(notification_id)
72 except exceptions.DatabaseException: 72 except exceptions.DatabaseException:
73 log.debug('Database Error. Attempting reconnect') 73 LOG.debug('Database Error. Attempting reconnect')
74 stored_notification = db_repo.get_notification(notification_id) 74 stored_notification = db_repo.get_notification(notification_id)
75 75
76 return stored_notification 76 return stored_notification
77 77
78 78
79def get_statsd_client(config, dimensions=None): 79def get_statsd_client(dimensions=None):
80 local_dims = dimensions.copy() if dimensions else {} 80 local_dims = dimensions.copy() if dimensions else {}
81 local_dims.update(NOTIFICATION_DIMENSIONS) 81 local_dims.update(NOTIFICATION_DIMENSIONS)
82 if 'statsd' in config: 82 client = monascastatsd.Client(name='monasca',
83 client = monascastatsd.Client(name='monasca', 83 host=CONF.statsd.host,
84 host=config['statsd'].get('host', 'localhost'), 84 port=CONF.statsd.port,
85 port=config['statsd'].get('port', 8125), 85 dimensions=local_dims)
86 dimensions=local_dims) 86
87 else:
88 client = monascastatsd.Client(name='monasca',
89 dimensions=local_dims)
90 return client 87 return client
diff --git a/monasca_notification/conf/__init__.py b/monasca_notification/conf/__init__.py
new file mode 100644
index 0000000..b20e5b2
--- /dev/null
+++ b/monasca_notification/conf/__init__.py
@@ -0,0 +1,162 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import collections
16import copy
17
18from oslo_config import cfg
19from oslo_log import log
20from oslo_utils import importutils
21
22from monasca_notification.conf import cli
23from monasca_notification.conf import database
24from monasca_notification.conf import kafka
25from monasca_notification.conf import notifiers
26from monasca_notification.conf import processors
27from monasca_notification.conf import queues
28from monasca_notification.conf import retry
29from monasca_notification.conf import statsd
30from monasca_notification.conf import zookeeper
31
32LOG = log.getLogger(__name__)
33CONF = cfg.CONF
34
35CONF_OPTS = [
36 cli,
37 database,
38 kafka,
39 notifiers,
40 processors,
41 queues,
42 retry,
43 statsd,
44 zookeeper
45]
46
47
48def register_opts(conf=None):
49 if conf is None:
50 conf = CONF
51 for m in CONF_OPTS:
52 m.register_opts(conf)
53
54
55def list_opts():
56 opts = collections.defaultdict(list)
57 for m in CONF_OPTS:
58 configs = copy.deepcopy(m.list_opts())
59 for key, val in configs.items():
60 opts[key].extend(val)
61 return _tupleize(opts)
62
63
64def load_from_yaml(yaml_config, conf=None):
65 # build named BACKWARD_MAP to modules set_defaults
66
67 if conf is None:
68 conf = CONF
69
70 def _noop(*arg, **kwargs):
71 pass
72
73 def _plain_override(g=None, **opts):
74 for k, v in opts.items():
75 conf.set_override(group=g, name=k, override=v)
76
77 def _load_plugin_settings(**notifiers_cfg):
78 notifiers_cfg = {t.lower(): v for t, v in notifiers_cfg.items()}
79 enabled_plugins = notifiers_cfg.pop('plugins', [])
80
81 # We still can have these 3 (email, pagerduty, webhook)
82 # that are considered as builtin, hence should be always enabled
83 conf_to_plugin = {
84 'email': 'monasca_notification.plugins.'
85 'email_notifier:EmailNotifier',
86 'pagerduty': 'monasca_notification.plugins.'
87 'pagerduty_notifier:PagerdutyNotifier',
88 'webhook': 'monasca_notification.plugins.'
89 'webhook_notifier:WebhookNotifier'
90 }
91 for ctp_key, ctp_clazz in conf_to_plugin.items():
92 if ctp_key in notifiers_cfg and ctp_key not in enabled_plugins:
93 LOG.debug('%s enabled as builtin plugin', ctp_key)
94 enabled_plugins.append(ctp_clazz)
95
96 _plain_override(g='notification_types', enabled=enabled_plugins)
97 if not enabled_plugins:
98 return
99
100 for ep in enabled_plugins:
101 ep_module = importutils.import_module(ep.split(':')[0])
102 ep_clazz = importutils.import_class(ep.replace(':', '.'))
103
104 if not hasattr(ep_module, 'register_opts'):
105 LOG.debug('%s does not have \'register_opts\' method')
106 continue
107 if not hasattr(ep_clazz, 'type'):
108 LOG.debug('%s does not have \'type\' class variable')
109 continue
110
111 ep_r_opt = getattr(ep_module, 'register_opts')
112 ep_type = getattr(ep_clazz, 'type')
113
114 ep_r_opt(conf) # register options
115 _plain_override(g='%s_notifier' % ep_type,
116 **notifiers_cfg.get(ep_type))
117
118 LOG.debug('Registered options and values of the %s notifier',
119 ep_type)
120
121 def _configure_and_warn_the_logging(logging_config):
122 LOG.warning('Configuration of the logging system from '
123 '\'notification.yml\' has been deprecated and '
124 'Please check how to configure logging with '
125 'oslo.log library.')
126 import logging.config
127 logging.config.dictConfig(logging_config)
128
129 mappper = {
130 'statsd': [lambda d: _plain_override(g='statsd', **d)],
131 'retry': [lambda d: _plain_override(g='retry_engine', **d)],
132 'database': [
133 lambda d: _plain_override(g='database', repo_driver=d['repo_driver']),
134 lambda d: _plain_override(g='orm', url=d['orm']['url'])
135 ],
136 'postgresql': [lambda d: _plain_override(g='postgresql', **d)],
137 'mysql': [lambda d: _plain_override(g='mysql', **d)],
138 'processors': [
139 lambda d: _plain_override(g='alarm_processor',
140 number=d['alarm']['number'],
141 ttl=d['alarm']['ttl']),
142 lambda d: _plain_override(g='notification_processor',
143 number=d['notification']['number'])
144 ],
145 'queues': [lambda d: _plain_override(g='queues', **d)],
146 'kafka': [lambda d: _plain_override(g='kafka', **d)],
147 'zookeeper': [lambda d: _plain_override(g='zookeeper', **d)],
148 'notification_types': [lambda d: _load_plugin_settings(**d)],
149 'logging': [_configure_and_warn_the_logging]
150 }
151
152 for key, opts in yaml_config.items():
153 LOG.debug('Loading group %s from deprecated yaml configuration', key)
154 handlers = mappper.get(key, [_noop])
155 if len(handlers) == 1 and handlers[0] == _noop:
156 LOG.warning('Unmapped configuration group %s from YAML file', key)
157 [handler(opts) for handler in handlers]
158
159
160def _tupleize(d):
161 """Convert a dict of options to the 2-tuple format."""
162 return [(key, value) for key, value in d.items()]
diff --git a/monasca_notification/conf/cli.py b/monasca_notification/conf/cli.py
new file mode 100644
index 0000000..572dfca
--- /dev/null
+++ b/monasca_notification/conf/cli.py
@@ -0,0 +1,40 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17cli_opts = [
18 cfg.StrOpt(name='yaml_config', default=None,
19 positional=True,
20 help='Backward compatible option that allows to pass path '
21 'to YAML file containing configuration '
22 'of monasca-notitifcation.',
23 deprecated_for_removal=True,
24 deprecated_since='1.9.0',
25 deprecated_reason='monasca-notification has moved to '
26 'oslo.conf henceusing YAML based '
27 'configuration will be removed '
28 'after PIKE release.')
29]
30
31
32def register_opts(conf):
33 for opt in cli_opts:
34 conf.register_cli_opt(opt=opt)
35
36
37def list_opts():
38 return {
39 'default': cli_opts
40 }
diff --git a/monasca_notification/conf/database.py b/monasca_notification/conf/database.py
new file mode 100644
index 0000000..2a654f2
--- /dev/null
+++ b/monasca_notification/conf/database.py
@@ -0,0 +1,129 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17from monasca_notification.conf import types
18
19_POSTGRESQL = 'postgresql'
20_MYSQL = 'mysql'
21_ORM = 'orm'
22_REPO_DRIVERS_MAP = {
23 _POSTGRESQL: 'monasca_notification.common.repositories.'
24 'postgres.pgsql_repo:PostgresqlRepo',
25 _MYSQL: 'monasca_notification.common.repositories.'
26 'mysql.mysql_repo:MysqlRepo',
27 _ORM: 'monasca_notification.common.repositories.'
28 'orm.orm_repo:OrmRepo'
29}
30_ACCEPTABLE_DRIVER_KEYS = set(list(_REPO_DRIVERS_MAP.keys()) +
31 list(_REPO_DRIVERS_MAP.values()))
32
33_DEFAULT_DB_HOST = '127.0.0.1'
34_DEFAULT_DB_USER = 'notification'
35_DEFAULT_DB_PASSWORD = 'password'
36_DEFAULT_DB_NAME = 'mon'
37_DEFAULT_POSTGRESQL_PORT = 5432
38_DEFAULT_MYSQL_PORT = 3306
39
40db_group = cfg.OptGroup('database',
41 title='Database Options',
42 help='Driver configuration for database connectivity.')
43
44db_opts = [
45 types.PluginOpt(name='repo_driver', choices=_ACCEPTABLE_DRIVER_KEYS,
46 default=_MYSQL, plugin_map=_REPO_DRIVERS_MAP,
47 required=True, advanced=True,
48 help='Driver name (or full class path) that should be '
49 'used to handle RDB connections. Accepts either '
50 'short labels {0} or full class names {1}. '
51 'Configuring either of them will require presence of '
52 'one of following sections {0} inside configuration '
53 'file.'.format(_REPO_DRIVERS_MAP.keys(),
54 _REPO_DRIVERS_MAP.values())
55 )
56]
57
58orm_group = cfg.OptGroup('orm',
59 title='ORM Options',
60 help='Configuration options to configure '
61 'ORM RBD driver.')
62orm_opts = [
63 cfg.StrOpt(name='url', default=None,
64 help='Connection string for sqlalchemy.')
65]
66
67mysql_group = cfg.OptGroup('mysql',
68 title='MySQL Options',
69 help='Configuration options to configure '
70 'plain MySQL RBD driver.')
71mysql_opts = [
72 cfg.HostAddressOpt(name='host', default=_DEFAULT_DB_HOST,
73 help='IP address of MySQL instance.'),
74 cfg.PortOpt(name='port', default=_DEFAULT_MYSQL_PORT,
75 help='Port number of MySQL instance.'),
76 cfg.StrOpt(name='user', default=_DEFAULT_DB_USER,
77 help='Username to connect to MySQL '
78 'instance and given database.'),
79 cfg.StrOpt(name='passwd', default=_DEFAULT_DB_PASSWORD,
80 ignore_case=True, secret=True,
81 help='Password to connect to MySQL instance '
82 'and given database.'),
83 cfg.DictOpt(name='ssl', default={},
84 help='A dict of arguments similar '
85 'to mysql_ssl_set parameters.'),
86 cfg.StrOpt(name='db', default=_DEFAULT_DB_NAME,
87 help='Database name available in given MySQL instance.')
88]
89
90postgresql_group = cfg.OptGroup('postgresql',
91 title='PostgreSQL Options',
92 help='Configuration options to configure '
93 'plain PostgreSQL RBD driver.')
94postgresql_opts = [
95 cfg.HostAddressOpt(name='host', default=_DEFAULT_DB_HOST,
96 help='IP address of PostgreSQL instance.'),
97 cfg.PortOpt(name='port', default=_DEFAULT_POSTGRESQL_PORT,
98 help='Port number of PostgreSQL instance.'),
99 cfg.StrOpt(name='user', default=_DEFAULT_DB_USER,
100 help='Username to connect to PostgreSQL '
101 'instance and given database.'),
102 cfg.StrOpt(name='password', default=_DEFAULT_DB_PASSWORD,
103 secret=True, help='Password to connect to PostgreSQL '
104 'instance and given database'),
105 cfg.StrOpt(name='database', default=_DEFAULT_DB_NAME,
106 help='Database name available in '
107 'given PostgreSQL instance.')
108]
109
110
111def register_opts(conf):
112 conf.register_group(db_group)
113 conf.register_group(orm_group)
114 conf.register_group(mysql_group)
115 conf.register_group(postgresql_group)
116
117 conf.register_opts(db_opts, group=db_group)
118 conf.register_opts(orm_opts, group=orm_group)
119 conf.register_opts(mysql_opts, group=mysql_group)
120 conf.register_opts(postgresql_opts, group=postgresql_group)
121
122
123def list_opts():
124 return {
125 db_group: db_opts,
126 orm_group: orm_opts,
127 mysql_group: mysql_opts,
128 postgresql_group: postgresql_opts,
129 }
diff --git a/monasca_notification/conf/kafka.py b/monasca_notification/conf/kafka.py
new file mode 100644
index 0000000..fdee2ac
--- /dev/null
+++ b/monasca_notification/conf/kafka.py
@@ -0,0 +1,77 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17from monasca_notification.conf import types
18
19_DEFAULT_URL = '127.0.0.1:9092'
20_DEFAULT_GROUP = 'monasca-notification'
21_DEFAULT_ALARM_TOPIC = 'alarm-state-transitions'
22_DEFAULT_NOTIFICATION_TOPIC = 'alarm-notifications'
23_DEFAULT_RETRY_TOPIC = 'retry-notifications'
24_DEFAULT_PERIODIC_TOPICS = {
25 60: '60-seconds-notifications'
26}
27_DEFAULT_MAX_OFFSET_LAG = 600
28
29kafka_group = cfg.OptGroup('kafka',
30 title='Kafka Options',
31 help='Options under this group allow to configure '
32 'valid connection or Kafka queue.')
33
34kafka_opts = [
35 cfg.ListOpt(name='url', item_type=types.HostAddressPortType(),
36 bounds=False,
37 default=_DEFAULT_URL, required=True,
38 help='List of addresses (with ports) pointing '
39 'at zookeeper cluster.'),
40 cfg.StrOpt(name='group', default=_DEFAULT_GROUP,
41 required=True, advanced=True,
42 help='Consumer\'s group for monasca-notification client.'),
43 cfg.StrOpt(name='alarm_topic', default=_DEFAULT_ALARM_TOPIC,
44 required=True, advanced=True,
45 help='Topic name in kafka where alarm '
46 'transitions are stored.'),
47 cfg.StrOpt(name='notification_topic', default=_DEFAULT_NOTIFICATION_TOPIC,
48 required=True, advanced=True,
49 help='Topic name in kafka where alarm '
50 'notifications are stored.'),
51 cfg.StrOpt(name='notification_retry_topic', default=_DEFAULT_RETRY_TOPIC,
52 required=True, advanced=True,
53 help='Topic name in kafka where notifications, that have '
54 'failed to be sent and are waiting for retry operations, '
55 'are stored.'),
56 cfg.DictOpt(name='periodic', default=_DEFAULT_PERIODIC_TOPICS,
57 required=True, advanced=True,
58 help='Dict of periodic topics. Keys are the period and '
59 'values the actual topic names in kafka where '
60 'notifications are stored.'),
61 cfg.IntOpt(name='max_offset_lag', default=_DEFAULT_MAX_OFFSET_LAG,
62 required=True, advanced=True,
63 help='Maximum lag for topic that is acceptable by '
64 'the monasca-notification. Notifications that are older '
65 'than this offset are skipped.')
66]
67
68
69def register_opts(conf):
70 conf.register_group(kafka_group)
71 conf.register_opts(kafka_opts, group=kafka_group)
72
73
74def list_opts():
75 return {
76 kafka_group: kafka_opts
77 }
diff --git a/monasca_notification/conf/notifiers.py b/monasca_notification/conf/notifiers.py
new file mode 100644
index 0000000..6f2787a
--- /dev/null
+++ b/monasca_notification/conf/notifiers.py
@@ -0,0 +1,53 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17# NOTE(kornicameister) Until https://review.openstack.org/#/c/435136/
18# is merged we only treat these below as plugins.
19# WEBHOOK, EMAIL, PAGERDUTY are now treated as built-in & hardcoded
20# user has no possibility of enabling/disabling them
21
22_KEY_MAP = {
23 'hipchat': 'monasca_notification.plugins.hipchat_notifier.HipChatNotifier',
24 'slack': 'monasca_notification.plugins.slack_notifier.SlackNotifier',
25 'jira': 'monasca_notification.plugins.jira_notifier.JiraNotifier'
26}
27
28notifier_group = cfg.OptGroup('notification_types',
29 title='Notification types',
30 help='Group allows to configure available '
31 'notifiers inside notification engine.')
32
33notifier_opts = [
34 cfg.ListOpt(name='enabled', default=[],
35 item_type=lambda x: _KEY_MAP.get(x, x), bounds=False,
36 advanced=True, sample_default=','.join(_KEY_MAP.keys()),
37 help='List of enabled notification types. You may specify '
38 'full class name {} '
39 'or shorter label {}.'.format(_KEY_MAP.get('hipchat'),
40 'hipchat')
41 )
42]
43
44
45def register_opts(conf):
46 conf.register_group(notifier_group)
47 conf.register_opts(notifier_opts, group=notifier_group)
48
49
50def list_opts():
51 return {
52 notifier_group: notifier_opts,
53 }
diff --git a/monasca_notification/conf/processors.py b/monasca_notification/conf/processors.py
new file mode 100644
index 0000000..ece177d
--- /dev/null
+++ b/monasca_notification/conf/processors.py
@@ -0,0 +1,55 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17ap_group = cfg.OptGroup('alarm_processor',
18 title='Alarm processor group',
19 help='Options to configure alarm processor.')
20
21ap_opts = [
22 cfg.IntOpt(name='number', min=1, default=2,
23 help='Number of alarm processors to spawn',
24 deprecated_for_removal=True,
25 deprecated_since='1.8.0',
26 deprecated_reason='Options is not used in the current code '
27 'and will be removed in future releases.'),
28 cfg.IntOpt(name='ttl', default=14400,
29 advanced=True,
30 help='Alarms older than TTL are not processed '
31 'by notification engine.')
32]
33
34np_group = cfg.OptGroup('notification_processor',
35 title='Notification processor group',
36 help='Options to configure notification processor.')
37np_opts = [
38 cfg.IntOpt(name='number', min=1,
39 default=4, help='Number of notification processors to spawn.')
40]
41
42
43def register_opts(conf):
44 conf.register_group(ap_group)
45 conf.register_group(np_group)
46
47 conf.register_opts(ap_opts, group=ap_group)
48 conf.register_opts(np_opts, group=np_group)
49
50
51def list_opts():
52 return {
53 ap_group: ap_opts,
54 np_group: np_opts
55 }
diff --git a/monasca_notification/conf/queues.py b/monasca_notification/conf/queues.py
new file mode 100644
index 0000000..96c59e7
--- /dev/null
+++ b/monasca_notification/conf/queues.py
@@ -0,0 +1,45 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17queues_group = cfg.OptGroup('queues',
18 title='Queues Options',
19 help=('Options under this group allow to '
20 'configure valid connection sizes of '
21 'all queues.'))
22
23queues_opts = [
24 cfg.IntOpt(name='alarms_size', min=1, default=256,
25 help='Size of the alarms queue.'),
26 cfg.IntOpt(name='finished_size', min=1, default=256,
27 help='Size of the finished alarms queue.'),
28 cfg.IntOpt(name='notifications_size', min=1, default=256,
29 help='Size of notifications queue.'),
30 cfg.IntOpt(name='sent_notifications_size', min=1, default=50,
31 help='Size of sent notifications queue. '
32 'Limiting this size reduces potential or '
33 're-sent notifications after a failure.')
34]
35
36
37def register_opts(conf):
38 conf.register_group(queues_group)
39 conf.register_opts(queues_opts, group=queues_group)
40
41
42def list_opts():
43 return {
44 queues_group: queues_opts
45 }
diff --git a/monasca_notification/conf/retry.py b/monasca_notification/conf/retry.py
new file mode 100644
index 0000000..83b3ff9
--- /dev/null
+++ b/monasca_notification/conf/retry.py
@@ -0,0 +1,41 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17retry_engine_group = cfg.OptGroup('retry_engine',
18 title='Retry Engine Options',
19 help='Options under this group allow to '
20 'configure valid connection '
21 'for retry engine.')
22
23retry_opts = [
24 cfg.IntOpt(name='interval', min=30, default=30,
25 advanced=True,
26 help='How often should retry happen.'),
27 cfg.IntOpt(name='max_attempts', default=5,
28 advanced=True,
29 help='How many times should retrying be tried.')
30]
31
32
33def register_opts(conf):
34 conf.register_group(retry_engine_group)
35 conf.register_opts(retry_opts, group=retry_engine_group)
36
37
38def list_opts():
39 return {
40 retry_engine_group: retry_opts
41 }
diff --git a/monasca_notification/conf/statsd.py b/monasca_notification/conf/statsd.py
new file mode 100644
index 0000000..5d5275c
--- /dev/null
+++ b/monasca_notification/conf/statsd.py
@@ -0,0 +1,41 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17_DEFAULT_HOST = '127.0.0.1'
18_DEFAULT_PORT = 8125
19
20statsd_group = cfg.OptGroup('statsd',
21 title='statsd Options',
22 help='Options under this group allow '
23 'to configure valid connection '
24 'to statsd server launched by monasca-agent.')
25
26statsd_opts = [
27 cfg.HostAddressOpt('host', default=_DEFAULT_HOST,
28 help='IP address of statsd server.'),
29 cfg.PortOpt('port', default=_DEFAULT_PORT, help='Port of statsd server.'),
30]
31
32
33def register_opts(conf):
34 conf.register_group(statsd_group)
35 conf.register_opts(statsd_opts, group=statsd_group)
36
37
38def list_opts():
39 return {
40 statsd_group: statsd_opts
41 }
diff --git a/monasca_notification/conf/types.py b/monasca_notification/conf/types.py
new file mode 100644
index 0000000..44b5b44
--- /dev/null
+++ b/monasca_notification/conf/types.py
@@ -0,0 +1,102 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16from oslo_config import types
17from oslo_log import log
18from oslo_utils import importutils
19
20LOG = log.getLogger(__name__)
21
22
23class Plugin(types.String):
24 def __init__(self, ignore_missing=False, choices=None, plugin_map=None):
25
26 if not plugin_map:
27 # since simport is used, we cannot tell where plugin is located
28 # thus plugin map wouldn't contain it
29 plugin_map = {}
30
31 super(Plugin, self).__init__(choices, quotes=False, ignore_case=True,
32 type_name='plugin value')
33 self._plugin_map = plugin_map
34 self._ingore_mission = ignore_missing
35
36 def __call__(self, value):
37 value = super(Plugin, self).__call__(value)
38 value = self._get_actual_target(value)
39 cls = None
40
41 try:
42 value = value.replace(':', '.')
43 cls = importutils.import_class(value)
44 except ImportError:
45 if not self._ingore_mission:
46 raise ValueError('%s cannot be imported' % value)
47 else:
48 LOG.exception('%s cannot be imported', value)
49
50 return cls
51
52 def _get_actual_target(self, value):
53
54 # NOTE(trebskit) missing values will be handled
55 # by choices from StringType
56
57 if value in self._plugin_map.keys():
58 return self._plugin_map[value]
59
60 return value
61
62
63class PluginOpt(cfg.Opt):
64 def __init__(self, name, choices=None, plugin_map=None, **kwargs):
65 plugin = Plugin(choices=choices, plugin_map=plugin_map)
66 super(PluginOpt, self).__init__(name,
67 type=plugin,
68 **kwargs)
69
70
71class HostAddressPortType(types.HostAddress):
72 """HostAddress with additional port"""
73
74 def __init__(self, version=None):
75 type_name = 'host address port value'
76 super(HostAddressPortType, self).__init__(version,
77 type_name=type_name)
78
79 def __call__(self, value):
80 addr, port = value.split(':')
81
82 addr = self._validate_addr(addr)
83 port = self._validate_port(port)
84
85 if addr and port:
86 return '%s:%d' % (addr, port)
87 raise ValueError('%s is not valid host address with optional port')
88
89 @staticmethod
90 def _validate_port(port=80):
91 port = types.Port()(port)
92 return port
93
94 def _validate_addr(self, addr):
95 try:
96 addr = self.ip_address(addr)
97 except ValueError:
98 try:
99 addr = self.hostname(addr)
100 except ValueError:
101 raise ValueError("%s is not a valid host address", addr)
102 return addr
diff --git a/monasca_notification/conf/zookeeper.py b/monasca_notification/conf/zookeeper.py
new file mode 100644
index 0000000..60c4a9a
--- /dev/null
+++ b/monasca_notification/conf/zookeeper.py
@@ -0,0 +1,61 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_config import cfg
16
17from monasca_notification.conf import types
18
19_DEFAULT_URL = '127.0.0.1:2181'
20_DEFAULT_NOTIFICATION_PATH = '/notification/alarms'
21_DEFAULT_RETRY_PATH = '/notification/retry'
22_DEFAULT_PERIODIC_PATH = {
23 60: '/notification/60_seconds'
24}
25
26zookeeper_group = cfg.OptGroup('zookeeper',
27 title='Zookeeper Options',
28 help='Options under this group allow to '
29 'configure settings for zookeeper '
30 'handling.')
31
32zookeeper_opts = [
33 cfg.ListOpt(name='url', item_type=types.HostAddressPortType(),
34 default=_DEFAULT_URL, required=True,
35 help='List of addresses (with ports) pointing '
36 'at zookeeper cluster.'),
37 cfg.StrOpt(name='notification_path', default=_DEFAULT_NOTIFICATION_PATH,
38 required=True, advanced=True,
39 help='Path in zookeeper tree to track notification offsets.'),
40 cfg.StrOpt(name='notification_retry_path', default=_DEFAULT_RETRY_PATH,
41 required=True, advanced=True,
42 help='Path in zookeeper tree to track notification '
43 'retries offsets.'),
44 cfg.DictOpt(name='periodic_path', default=_DEFAULT_PERIODIC_PATH,
45 required=True, advanced=True,
46 help='Paths in zookeeper tree to track periodic offsets. '
47 'Keys must be integers describing the interval '
48 'of periodic notification. Values are actual '
49 'paths inside zookeeper tree.')
50]
51
52
53def register_opts(conf):
54 conf.register_group(zookeeper_group)
55 conf.register_opts(zookeeper_opts, group=zookeeper_group)
56
57
58def list_opts():
59 return {
60 zookeeper_group: zookeeper_opts
61 }
diff --git a/monasca_notification/config.py b/monasca_notification/config.py
new file mode 100644
index 0000000..9adee1b
--- /dev/null
+++ b/monasca_notification/config.py
@@ -0,0 +1,64 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from oslo_log import log
16import yaml
17
18from monasca_notification import conf
19from monasca_notification import version
20
21LOG = log.getLogger(__name__)
22CONF = conf.CONF
23_CONF_LOADED = False
24
25
26def parse_args(argv, no_yaml=False):
27 """Sets up configuration of monasca-notification."""
28
29 global _CONF_LOADED
30 if _CONF_LOADED:
31 LOG.debug('Configuration has been already loaded')
32 return
33
34 conf.register_opts(CONF)
35 log.register_options(CONF)
36
37 default_log_levels = (log.get_default_log_levels())
38 log.set_defaults(default_log_levels=default_log_levels)
39
40 CONF(args=argv,
41 project='monasca',
42 prog='notification',
43 version=version.version_string,
44 description='''
45 monasca-notification is an engine responsible for
46 transforming alarm transitions into proper notifications
47 ''')
48 log.setup(CONF,
49 product_name='monasca-notification',
50 version=version.version_string)
51
52 if not no_yaml:
53 # note(trebskit) used only in test cases as the notification.yml
54 # will be dropped eventually
55 set_from_yaml()
56
57 _CONF_LOADED = True
58
59
60def set_from_yaml():
61 if CONF.yaml_config:
62 LOG.info('Detected usage of deprecated YAML configuration')
63 yaml_cfg = yaml.safe_load(open(CONF.yaml_config, 'rb'))
64 conf.load_from_yaml(yaml_cfg)
diff --git a/monasca_notification/main.py b/monasca_notification/main.py
index 8023ff4..8f091a1 100644
--- a/monasca_notification/main.py
+++ b/monasca_notification/main.py
@@ -18,20 +18,23 @@
18 This engine reads alarms from Kafka and then notifies the customer using their configured notification method. 18 This engine reads alarms from Kafka and then notifies the customer using their configured notification method.
19""" 19"""
20 20
21import logging
22import logging.config
23import multiprocessing 21import multiprocessing
24import os 22import os
25import signal 23import signal
26import sys 24import sys
27import time 25import time
28import yaml 26import warnings
29 27
30from monasca_notification.notification_engine import NotificationEngine 28from oslo_log import log
31from monasca_notification.periodic_engine import PeriodicEngine 29
32from monasca_notification.retry_engine import RetryEngine 30from monasca_notification import config
31from monasca_notification import notification_engine
32from monasca_notification import periodic_engine
33from monasca_notification import retry_engine
34
35LOG = log.getLogger(__name__)
36CONF = config.CONF
33 37
34log = logging.getLogger(__name__)
35processors = [] # global list to facilitate clean signal handling 38processors = [] # global list to facilitate clean signal handling
36exiting = False 39exiting = False
37 40
@@ -45,10 +48,10 @@ def clean_exit(signum, frame=None):
45 # Since this is set up as a handler for SIGCHLD when this kills one 48 # Since this is set up as a handler for SIGCHLD when this kills one
46 # child it gets another signal, the global exiting avoids this running 49 # child it gets another signal, the global exiting avoids this running
47 # multiple times. 50 # multiple times.
48 log.debug('Exit in progress clean_exit received additional signal %s' % signum) 51 LOG.debug('Exit in progress clean_exit received additional signal %s' % signum)
49 return 52 return
50 53
51 log.info('Received signal %s, beginning graceful shutdown.' % signum) 54 LOG.info('Received signal %s, beginning graceful shutdown.' % signum)
52 exiting = True 55 exiting = True
53 wait_for_exit = False 56 wait_for_exit = False
54 57
@@ -68,7 +71,7 @@ def clean_exit(signum, frame=None):
68 71
69 # Kill everything, that didn't already die 72 # Kill everything, that didn't already die
70 for child in multiprocessing.active_children(): 73 for child in multiprocessing.active_children():
71 log.debug('Killing pid %s' % child.pid) 74 LOG.debug('Killing pid %s' % child.pid)
72 try: 75 try:
73 os.kill(child.pid, signal.SIGKILL) 76 os.kill(child.pid, signal.SIGKILL)
74 except Exception: # nosec 77 except Exception: # nosec
@@ -82,50 +85,35 @@ def clean_exit(signum, frame=None):
82 sys.exit(signum) 85 sys.exit(signum)
83 86
84 87
85def start_process(process_type, config, *args): 88def start_process(process_type, *args):
86 log.info("start process: {}".format(process_type)) 89 LOG.info("start process: {}".format(process_type))
87 p = process_type(config, *args) 90 p = process_type(*args)
88 p.run() 91 p.run()
89 92
90 93
91def main(argv=None): 94def main(argv=None):
92 if argv is None: 95 warnings.simplefilter('always')
93 argv = sys.argv 96 config.parse_args(argv=argv)
94 if len(argv) == 2: 97
95 config_file = argv[1] 98 for proc in range(0, CONF.notification_processor.number):
96 elif len(argv) > 2:
97 print("Usage: " + argv[0] + " <config_file>")
98 print("Config file defaults to /etc/monasca/notification.yaml")
99 return 1
100 else:
101 config_file = '/etc/monasca/notification.yaml'
102
103 config = yaml.safe_load(open(config_file, 'rb'))
104
105 # Setup logging
106 try:
107 if config['logging']['raise_exceptions'] is True:
108 logging.raiseExceptions = True
109 else:
110 logging.raiseExceptions = False
111 except KeyError:
112 logging.raiseExceptions = False
113 pass
114 logging.config.dictConfig(config['logging'])
115
116 for proc in range(0, config['processors']['notification']['number']):
117 processors.append(multiprocessing.Process( 99 processors.append(multiprocessing.Process(
118 target=start_process, args=(NotificationEngine, config))) 100 target=start_process,
101 args=(notification_engine.NotificationEngine,))
102 )
119 103
120 processors.append(multiprocessing.Process( 104 processors.append(multiprocessing.Process(
121 target=start_process, args=(RetryEngine, config))) 105 target=start_process,
106 args=(retry_engine.RetryEngine,))
107 )
122 108
123 if 60 in config['kafka']['periodic']: 109 if 60 in CONF.kafka.periodic:
124 processors.append(multiprocessing.Process( 110 processors.append(multiprocessing.Process(
125 target=start_process, args=(PeriodicEngine, config, 60))) 111 target=start_process,
112 args=(periodic_engine.PeriodicEngine, 60))
113 )
126 114
127 try: 115 try:
128 log.info('Starting processes') 116 LOG.info('Starting processes')
129 for process in processors: 117 for process in processors:
130 process.start() 118 process.start()
131 119
@@ -139,8 +127,9 @@ def main(argv=None):
139 time.sleep(10) 127 time.sleep(10)
140 128
141 except Exception: 129 except Exception:
142 log.exception('Error! Exiting.') 130 LOG.exception('Error! Exiting.')
143 clean_exit(signal.SIGKILL) 131 clean_exit(signal.SIGKILL)
144 132
133
145if __name__ == "__main__": 134if __name__ == "__main__":
146 sys.exit(main()) 135 sys.exit(main(sys.argv[1:]))
diff --git a/monasca_notification/notification_engine.py b/monasca_notification/notification_engine.py
index 79b952a..3673560 100644
--- a/monasca_notification/notification_engine.py
+++ b/monasca_notification/notification_engine.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -13,44 +14,40 @@
13# See the License for the specific language governing permissions and 14# See the License for the specific language governing permissions and
14# limitations under the License. 15# limitations under the License.
15 16
16import logging
17import time 17import time
18 18
19from oslo_config import cfg
20from oslo_log import log as logging
21
19from monasca_common.kafka import consumer 22from monasca_common.kafka import consumer
20from monasca_common.kafka import producer 23from monasca_common.kafka import producer
21from monasca_notification.common.utils import get_statsd_client 24from monasca_notification.common.utils import get_statsd_client
22from monasca_notification.processors.alarm_processor import AlarmProcessor 25from monasca_notification.processors import alarm_processor as ap
23from monasca_notification.processors.notification_processor import NotificationProcessor 26from monasca_notification.processors import notification_processor as np
24 27
25log = logging.getLogger(__name__) 28log = logging.getLogger(__name__)
29CONF = cfg.CONF
26 30
27 31
28class NotificationEngine(object): 32class NotificationEngine(object):
29 def __init__(self, config): 33 def __init__(self):
30 self._topics = {} 34 self._statsd = get_statsd_client()
31 self._topics['notification_topic'] = config['kafka']['notification_topic']
32 self._topics['retry_topic'] = config['kafka']['notification_retry_topic']
33
34 self._statsd = get_statsd_client(config)
35 self._consumer = consumer.KafkaConsumer( 35 self._consumer = consumer.KafkaConsumer(
36 config['kafka']['url'], 36 CONF.kafka.url,
37 config['zookeeper']['url'], 37 ','.join(CONF.zookeeper.url),
38 config['zookeeper']['notification_path'], 38 CONF.zookeeper.notification_path,
39 config['kafka']['group'], 39 CONF.kafka.group,
40 config['kafka']['alarm_topic']) 40 CONF.kafka.alarm_topic)
41 self._producer = producer.KafkaProducer(config['kafka']['url']) 41 self._producer = producer.KafkaProducer(CONF.kafka.url)
42 self._alarm_ttl = config['processors']['alarm']['ttl'] 42 self._alarms = ap.AlarmProcessor()
43 self._alarms = AlarmProcessor(self._alarm_ttl, config) 43 self._notifier = np.NotificationProcessor()
44 self._notifier = NotificationProcessor(config)
45
46 self._config = config
47 44
48 def _add_periodic_notifications(self, notifications): 45 def _add_periodic_notifications(self, notifications):
49 for notification in notifications: 46 for notification in notifications:
50 topic = notification.periodic_topic 47 topic = notification.periodic_topic
51 if topic in self._config['kafka']['periodic'] and notification.type == "webhook": 48 if topic in CONF.kafka.periodic and notification.type == "webhook":
52 notification.notification_timestamp = time.time() 49 notification.notification_timestamp = time.time()
53 self._producer.publish(self._config['kafka']['periodic'][topic], 50 self._producer.publish(CONF.kafka.periodic[topic],
54 [notification.to_json()]) 51 [notification.to_json()])
55 52
56 def run(self): 53 def run(self):
@@ -62,9 +59,9 @@ class NotificationEngine(object):
62 self._add_periodic_notifications(notifications) 59 self._add_periodic_notifications(notifications)
63 60
64 sent, failed = self._notifier.send(notifications) 61 sent, failed = self._notifier.send(notifications)
65 self._producer.publish(self._topics['notification_topic'], 62 self._producer.publish(CONF.kafka.notification_topic,
66 [i.to_json() for i in sent]) 63 [i.to_json() for i in sent])
67 self._producer.publish(self._topics['retry_topic'], 64 self._producer.publish(CONF.kafka.notification_retry_topic,
68 [i.to_json() for i in failed]) 65 [i.to_json() for i in failed])
69 66
70 self._consumer.commit() 67 self._consumer.commit()
diff --git a/monasca_notification/periodic_engine.py b/monasca_notification/periodic_engine.py
index 4844848..1c86291 100644
--- a/monasca_notification/periodic_engine.py
+++ b/monasca_notification/periodic_engine.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -14,9 +15,11 @@
14# limitations under the License. 15# limitations under the License.
15 16
16import json 17import json
17import logging
18import time 18import time
19 19
20from oslo_config import cfg
21from oslo_log import log as logging
22
20from monasca_common.kafka import consumer 23from monasca_common.kafka import consumer
21from monasca_common.kafka import producer 24from monasca_common.kafka import producer
22from monasca_notification.common.repositories import exceptions 25from monasca_notification.common.repositories import exceptions
@@ -26,25 +29,26 @@ from monasca_notification.common.utils import get_statsd_client
26from monasca_notification.processors import notification_processor 29from monasca_notification.processors import notification_processor
27 30
28log = logging.getLogger(__name__) 31log = logging.getLogger(__name__)
32CONF = cfg.CONF
29 33
30 34
31class PeriodicEngine(object): 35class PeriodicEngine(object):
32 def __init__(self, config, period): 36 def __init__(self, period):
33 self._topic_name = config['kafka']['periodic'][period] 37 self._topic_name = CONF.kafka.periodic[period]
34 38
35 self._statsd = get_statsd_client(config) 39 self._statsd = get_statsd_client()
36 40
37 zookeeper_path = config['zookeeper']['periodic_path'][period] 41 zookeeper_path = CONF.zookeeper.periodic_path[period]
38 self._consumer = consumer.KafkaConsumer(config['kafka']['url'], 42 self._consumer = consumer.KafkaConsumer(CONF.kafka.url,
39 config['zookeeper']['url'], 43 ','.join(CONF.zookeeper.url),
40 zookeeper_path, 44 zookeeper_path,
41 config['kafka']['group'], 45 CONF.kafka.group,
42 self._topic_name) 46 self._topic_name)
43 47
44 self._producer = producer.KafkaProducer(config['kafka']['url']) 48 self._producer = producer.KafkaProducer(CONF.kafka.url)
45 49
46 self._notifier = notification_processor.NotificationProcessor(config) 50 self._notifier = notification_processor.NotificationProcessor()
47 self._db_repo = get_db_repo(config) 51 self._db_repo = get_db_repo()
48 self._period = period 52 self._period = period
49 53
50 def _keep_sending(self, alarm_id, original_state, type, period): 54 def _keep_sending(self, alarm_id, original_state, type, period):
diff --git a/monasca_notification/plugins/abstract_notifier.py b/monasca_notification/plugins/abstract_notifier.py
index 5426725..dfc9a0e 100644
--- a/monasca_notification/plugins/abstract_notifier.py
+++ b/monasca_notification/plugins/abstract_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -24,10 +25,6 @@ class AbstractNotifier(object):
24 pass 25 pass
25 26
26 @abc.abstractproperty 27 @abc.abstractproperty
27 def type(self):
28 pass
29
30 @abc.abstractproperty
31 def statsd_name(self): 28 def statsd_name(self):
32 pass 29 pass
33 30
diff --git a/monasca_notification/plugins/email_notifier.py b/monasca_notification/plugins/email_notifier.py
index f99e872..571716f 100644
--- a/monasca_notification/plugins/email_notifier.py
+++ b/monasca_notification/plugins/email_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -20,9 +21,13 @@ import six
20import smtplib 21import smtplib
21import time 22import time
22 23
24from debtcollector import removals
25from oslo_config import cfg
23 26
24from monasca_notification.plugins import abstract_notifier 27from monasca_notification.plugins import abstract_notifier
25 28
29CONF = cfg.CONF
30
26EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message} 31EMAIL_SINGLE_HOST_BASE = u'''On host "{hostname}" for target "{target_host}" {message}
27 32
28Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC 33Alarm "{alarm_name}" transitioned to the {state} state at {timestamp} UTC
@@ -60,23 +65,40 @@ With dimensions
60{metric_dimensions}''' 65{metric_dimensions}'''
61 66
62 67
68def register_opts(conf):
69 gr = cfg.OptGroup(name='%s_notifier' % EmailNotifier.type)
70 opts = [
71 cfg.StrOpt(name='from_addr'),
72 cfg.HostAddressOpt(name='server'),
73 cfg.PortOpt(name='port', default=25),
74 cfg.IntOpt(name='timeout', default=5, min=1),
75 cfg.StrOpt(name='user', default=None),
76 cfg.StrOpt(name='password', default=None, secret=True),
77 cfg.StrOpt(name='grafana_url', default=None)
78 ]
79
80 conf.register_group(gr)
81 conf.register_opts(opts, group=gr)
82
83
63class EmailNotifier(abstract_notifier.AbstractNotifier): 84class EmailNotifier(abstract_notifier.AbstractNotifier):
64 85
86 type = 'email'
87
65 def __init__(self, log): 88 def __init__(self, log):
66 super(EmailNotifier, self).__init__() 89 super(EmailNotifier, self).__init__()
67 self._log = log 90 self._log = log
68 self._smtp = None 91 self._smtp = None
69 self._config = None
70 92
71 def config(self, config): 93 @removals.remove(
72 self._config = config 94 message='Configuration of notifier is available through oslo.cfg',
95 version='1.9.0',
96 removal_version='3.0.0'
97 )
98 def config(self, config=None):
73 self._smtp_connect() 99 self._smtp_connect()
74 100
75 @property 101 @property
76 def type(self):
77 return "email"
78
79 @property
80 def statsd_name(self): 102 def statsd_name(self):
81 return "sent_smtp_count" 103 return "sent_smtp_count"
82 104
@@ -122,7 +144,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
122 return False 144 return False
123 145
124 def _sendmail(self, notification, msg): 146 def _sendmail(self, notification, msg):
125 self._smtp.sendmail(self._config['from_addr'], 147 self._smtp.sendmail(CONF.email_notifier.from_addr,
126 notification.address, 148 notification.address,
127 msg.as_string()) 149 msg.as_string())
128 self._log.debug("Sent email to {}, notification {}".format(notification.address, 150 self._log.debug("Sent email to {}, notification {}".format(notification.address,
@@ -135,15 +157,19 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
135 def _smtp_connect(self): 157 def _smtp_connect(self):
136 """Connect to the smtp server 158 """Connect to the smtp server
137 """ 159 """
138 self._log.info("Connecting to Email Server {}".format(self._config['server'])) 160 self._log.info("Connecting to Email Server {}".format(
161 CONF.email_notifier.server))
139 162
140 try: 163 try:
141 smtp = smtplib.SMTP(self._config['server'], 164 smtp = smtplib.SMTP(CONF.email_notifier.server,
142 self._config['port'], 165 CONF.email_notifier.port,
143 timeout=self._config['timeout']) 166 timeout=CONF.email_notifier.timeout)
144 167
145 if ('user', 'password') in self._config.keys(): 168 email_notifier_user = CONF.email_notifier.user
146 smtp.login(self._config['user'], self._config['password']) 169 email_notifier_password = CONF.email_notifier.password
170 if email_notifier_user and email_notifier_password:
171 smtp.login(email_notifier_user,
172 email_notifier_password)
147 173
148 self._smtp = smtp 174 self._smtp = smtp
149 return True 175 return True
@@ -222,7 +248,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
222 248
223 msg = email.mime.text.MIMEText(text, 'plain', 'utf-8') 249 msg = email.mime.text.MIMEText(text, 'plain', 'utf-8')
224 msg['Subject'] = email.header.Header(subject, 'utf-8') 250 msg['Subject'] = email.header.Header(subject, 'utf-8')
225 msg['From'] = self._config['from_addr'] 251 msg['From'] = CONF.email_notifier.from_addr
226 msg['To'] = notification.address 252 msg['To'] = notification.address
227 msg['Date'] = email.utils.formatdate(localtime=True, usegmt=True) 253 msg['Date'] = email.utils.formatdate(localtime=True, usegmt=True)
228 254
@@ -237,7 +263,7 @@ class EmailNotifier(abstract_notifier.AbstractNotifier):
237 has been defined. 263 has been defined.
238 """ 264 """
239 265
240 grafana_url = self._config.get('grafana_url', None) 266 grafana_url = CONF.email_notifier.grafana_url
241 if grafana_url is None: 267 if grafana_url is None:
242 return None 268 return None
243 269
diff --git a/monasca_notification/plugins/hipchat_notifier.py b/monasca_notification/plugins/hipchat_notifier.py
index fbd3e42..d9a2ea4 100644
--- a/monasca_notification/plugins/hipchat_notifier.py
+++ b/monasca_notification/plugins/hipchat_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -16,10 +17,13 @@
16import requests 17import requests
17import ujson as json 18import ujson as json
18 19
20from debtcollector import removals
21from oslo_config import cfg
19from six.moves import urllib 22from six.moves import urllib
20 23
21from monasca_notification.plugins import abstract_notifier 24from monasca_notification.plugins import abstract_notifier
22 25
26CONF = cfg.CONF
23 27
24""" 28"""
25 notification.address = https://hipchat.hpcloud.net/v2/room/<room_id>/notification?auth_token=432432 29 notification.address = https://hipchat.hpcloud.net/v2/room/<room_id>/notification?auth_token=432432
@@ -44,17 +48,34 @@ SEVERITY_COLORS = {"low": 'green',
44 'critical': 'red'} 48 'critical': 'red'}
45 49
46 50
51def register_opts(conf):
52 gr = cfg.OptGroup(name='%s_notifier' % HipChatNotifier.type)
53 opts = [
54 cfg.IntOpt(name='timeout', default=5, min=1),
55 cfg.BoolOpt(name='insecure', default=True),
56 cfg.StrOpt(name='ca_certs', default=None),
57 cfg.StrOpt(name='proxy', default=None)
58 ]
59
60 conf.register_group(gr)
61 conf.register_opts(opts, group=gr)
62
63
47class HipChatNotifier(abstract_notifier.AbstractNotifier): 64class HipChatNotifier(abstract_notifier.AbstractNotifier):
65
66 type = 'hipchat'
67
48 def __init__(self, log): 68 def __init__(self, log):
69 super(HipChatNotifier, self).__init__()
49 self._log = log 70 self._log = log
50 71
51 def config(self, config_dict): 72 @removals.remove(
52 self._config = {'timeout': 5} 73 message='Configuration of notifier is available through oslo.cfg',
53 self._config.update(config_dict) 74 version='1.9.0',
54 75 removal_version='3.0.0'
55 @property 76 )
56 def type(self): 77 def config(self, config_dict=None):
57 return "hipchat" 78 pass
58 79
59 @property 80 @property
60 def statsd_name(self): 81 def statsd_name(self):
@@ -97,14 +118,17 @@ class HipChatNotifier(abstract_notifier.AbstractNotifier):
97 url = urllib.parse.urljoin(notification.address, urllib.parse.urlparse(notification.address).path) 118 url = urllib.parse.urljoin(notification.address, urllib.parse.urlparse(notification.address).path)
98 119
99 # Default option is to do cert verification 120 # Default option is to do cert verification
100 verify = not self._config.get('insecure', True) 121 verify = not CONF.hipchat_notifier.insecure
122 ca_certs = CONF.hipchat_notifier.ca_certs
123 proxy = CONF.hipchat_notifier.proxy
124
101 # If ca_certs is specified, do cert validation and ignore insecure flag 125 # If ca_certs is specified, do cert validation and ignore insecure flag
102 if (self._config.get("ca_certs")): 126 if ca_certs is not None:
103 verify = self._config.get("ca_certs") 127 verify = ca_certs
104 128
105 proxyDict = None 129 proxyDict = None
106 if (self._config.get("proxy")): 130 if proxy is not None:
107 proxyDict = {"https": self._config.get("proxy")} 131 proxyDict = {'https': proxy}
108 132
109 try: 133 try:
110 # Posting on the given URL 134 # Posting on the given URL
@@ -113,7 +137,7 @@ class HipChatNotifier(abstract_notifier.AbstractNotifier):
113 verify=verify, 137 verify=verify,
114 params=query_params, 138 params=query_params,
115 proxies=proxyDict, 139 proxies=proxyDict,
116 timeout=self._config['timeout']) 140 timeout=CONF.hipchat_notifier.timeout)
117 141
118 if result.status_code in range(200, 300): 142 if result.status_code in range(200, 300):
119 self._log.info("Notification successfully posted.") 143 self._log.info("Notification successfully posted.")
diff --git a/monasca_notification/plugins/jira_notifier.py b/monasca_notification/plugins/jira_notifier.py
index 6bfcab3..86c2341 100644
--- a/monasca_notification/plugins/jira_notifier.py
+++ b/monasca_notification/plugins/jira_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP 1# (C) Copyright 2016 Hewlett Packard Enterprise Development Company LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -19,6 +20,9 @@ from six.moves import urllib
19import ujson as json 20import ujson as json
20import yaml 21import yaml
21 22
23from debtcollector import removals
24from oslo_config import cfg
25
22from monasca_notification.plugins.abstract_notifier import AbstractNotifier 26from monasca_notification.plugins.abstract_notifier import AbstractNotifier
23 27
24""" 28"""
@@ -51,27 +55,40 @@ from monasca_notification.plugins.abstract_notifier import AbstractNotifier
51""" 55"""
52 56
53 57
58CONF = cfg.CONF
59
60
61def register_opts(conf):
62 gr = cfg.OptGroup(name='%s_notifier' % JiraNotifier.type)
63 opts = [
64 cfg.IntOpt(name='timeout', default=5, min=1),
65 cfg.StrOpt(name='user', required=False),
66 cfg.StrOpt(name='password', required=False, secret=True),
67 cfg.StrOpt(name='custom_formatter', default=None),
68 cfg.StrOpt(name='proxy', default=None)
69 ]
70
71 conf.register_group(gr)
72 conf.register_opts(opts, group=gr)
73
74
54class JiraNotifier(AbstractNotifier): 75class JiraNotifier(AbstractNotifier):
55 76
77 type = 'jira'
56 _search_query = search_query = "project={} and reporter='{}' and summary ~ '{}'" 78 _search_query = search_query = "project={} and reporter='{}' and summary ~ '{}'"
57 79
58 def __init__(self, log): 80 def __init__(self, log):
81 super(JiraNotifier, self).__init__()
59 self._log = log 82 self._log = log
60 self.jira_fields_format = None 83 self.jira_fields_format = None
61 84
85 @removals.remove(
86 message='Configuration of notifier is available through oslo.cfg',
87 version='1.9.0',
88 removal_version='3.0.0'
89 )
62 def config(self, config_dict): 90 def config(self, config_dict):
63 self._config = {'timeout': 5} 91 pass
64 if not config_dict.get("user") and not config_dict.get("password"):
65 message = "Missing user and password settings in JIRA plugin configuration"
66 self._log.exception(message)
67 raise Exception(message)
68
69 self._config.update(config_dict)
70 self.jira_fields_format = self._get_jira_custom_format_fields()
71
72 @property
73 def type(self):
74 return "jira"
75 92
76 @property 93 @property
77 def statsd_name(self): 94 def statsd_name(self):
@@ -80,9 +97,10 @@ class JiraNotifier(AbstractNotifier):
80 def _get_jira_custom_format_fields(self): 97 def _get_jira_custom_format_fields(self):
81 jira_fields_format = None 98 jira_fields_format = None
82 99
83 if (not self.jira_fields_format and self._config.get("custom_formatter")): 100 formatter = CONF.jira_notifier.custom_formatter
101 if not self.jira_fields_format and formatter:
84 try: 102 try:
85 with open(self._config.get("custom_formatter")) as f: 103 with open(formatter, 'r') as f:
86 jira_fields_format = yaml.safe_load(f) 104 jira_fields_format = yaml.safe_load(f)
87 except Exception: 105 except Exception:
88 self._log.exception("Unable to read custom_formatter file. Check file location") 106 self._log.exception("Unable to read custom_formatter file. Check file location")
@@ -139,8 +157,10 @@ class JiraNotifier(AbstractNotifier):
139 return jira_fields 157 return jira_fields
140 158
141 def _build_jira_message(self, notification): 159 def _build_jira_message(self, notification):
142 if self._config.get("custom_formatter"): 160 formatter = CONF.jira_notifier.custom_formatter
143 return self._build_custom_jira_message(notification, self.jira_fields_format) 161 if formatter:
162 return self._build_custom_jira_message(notification,
163 self._get_jira_custom_format_fields())
144 164
145 return self._build_default_jira_message(notification) 165 return self._build_default_jira_message(notification)
146 166
@@ -159,13 +179,17 @@ class JiraNotifier(AbstractNotifier):
159 if query_params.get("component"): 179 if query_params.get("component"):
160 jira_fields["component"] = query_params["component"][0] 180 jira_fields["component"] = query_params["component"][0]
161 181
162 auth = (self._config["user"], self._config["password"]) 182 auth = (
163 proxyDict = None 183 CONF.jira_notifier.user,
164 if (self._config.get("proxy")): 184 CONF.jira_notifier.password
165 proxyDict = {"https": self._config.get("proxy")} 185 )
186 proxy = CONF.jira_notifier.proxy
187 proxy_dict = None
188 if proxy is not None:
189 proxy_dict = {"https": proxy}
166 190
167 try: 191 try:
168 jira_obj = jira.JIRA(url, basic_auth=auth, proxies=proxyDict) 192 jira_obj = jira.JIRA(url, basic_auth=auth, proxies=proxy_dict)
169 193
170 self.jira_workflow(jira_fields, jira_obj, notification) 194 self.jira_workflow(jira_fields, jira_obj, notification)
171 except Exception: 195 except Exception:
@@ -192,7 +216,8 @@ class JiraNotifier(AbstractNotifier):
192 issue_dict["components"] = [{"name": jira_fields.get("component")}] 216 issue_dict["components"] = [{"name": jira_fields.get("component")}]
193 217
194 search_term = self._search_query.format(issue_dict["project"]["key"], 218 search_term = self._search_query.format(issue_dict["project"]["key"],
195 self._config["user"], notification.alarm_id) 219 CONF.jira_notifier.user,
220 notification.alarm_id)
196 issue_list = jira_obj.search_issues(search_term) 221 issue_list = jira_obj.search_issues(search_term)
197 if not issue_list: 222 if not issue_list:
198 self._log.debug("Creating an issue with the data {}".format(issue_dict)) 223 self._log.debug("Creating an issue with the data {}".format(issue_dict))
diff --git a/monasca_notification/plugins/pagerduty_notifier.py b/monasca_notification/plugins/pagerduty_notifier.py
index 306a2ad..51fa738 100644
--- a/monasca_notification/plugins/pagerduty_notifier.py
+++ b/monasca_notification/plugins/pagerduty_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -16,25 +17,44 @@
16import requests 17import requests
17import ujson as json 18import ujson as json
18 19
20from debtcollector import removals
21from oslo_config import cfg
22
19from monasca_notification.plugins import abstract_notifier 23from monasca_notification.plugins import abstract_notifier
20 24
21 25
26CONF = cfg.CONF
22VALID_HTTP_CODES = [200, 201, 204] 27VALID_HTTP_CODES = [200, 201, 204]
23 28
24 29
30def register_opts(conf):
31 gr = cfg.OptGroup(name='%s_notifier' % PagerdutyNotifier.type)
32 opts = [
33 cfg.IntOpt(name='timeout', default=5, min=1),
34 cfg.StrOpt(name='url',
35 default='https://events.pagerduty.com/'
36 'generic/2010-04-15/create_event.json')
37 ]
38
39 conf.register_group(gr)
40 conf.register_opts(opts, group=gr)
41
42
25class PagerdutyNotifier(abstract_notifier.AbstractNotifier): 43class PagerdutyNotifier(abstract_notifier.AbstractNotifier):
44
45 type = 'pagerduty'
46
26 def __init__(self, log): 47 def __init__(self, log):
48 super(PagerdutyNotifier, self).__init__()
27 self._log = log 49 self._log = log
28 50
51 @removals.remove(
52 message='Configuration of notifier is available through oslo.cfg',
53 version='1.9.0',
54 removal_version='3.0.0'
55 )
29 def config(self, config): 56 def config(self, config):
30 self._config = { 57 pass
31 'timeout': 5,
32 'url': 'https://events.pagerduty.com/generic/2010-04-15/create_event.json'}
33 self._config.update(config)
34
35 @property
36 def type(self):
37 return "pagerduty"
38 58
39 @property 59 @property
40 def statsd_name(self): 60 def statsd_name(self):
@@ -44,7 +64,7 @@ class PagerdutyNotifier(abstract_notifier.AbstractNotifier):
44 """Send pagerduty notification 64 """Send pagerduty notification
45 """ 65 """
46 66
47 url = self._config['url'] 67 url = CONF.pagerduty_notifier.url
48 headers = {"content-type": "application/json"} 68 headers = {"content-type": "application/json"}
49 body = {"service_key": notification.address, 69 body = {"service_key": notification.address,
50 "event_type": "trigger", 70 "event_type": "trigger",
@@ -60,7 +80,7 @@ class PagerdutyNotifier(abstract_notifier.AbstractNotifier):
60 result = requests.post(url=url, 80 result = requests.post(url=url,
61 data=json.dumps(body), 81 data=json.dumps(body),
62 headers=headers, 82 headers=headers,
63 timeout=self._config['timeout']) 83 timeout=CONF.pagerduty_notifier.timeout)
64 84
65 if result.status_code in VALID_HTTP_CODES: 85 if result.status_code in VALID_HTTP_CODES:
66 return True 86 return True
diff --git a/monasca_notification/plugins/slack_notifier.py b/monasca_notification/plugins/slack_notifier.py
index 4b281f4..8173121 100644
--- a/monasca_notification/plugins/slack_notifier.py
+++ b/monasca_notification/plugins/slack_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -17,8 +18,26 @@ import requests
17from six.moves import urllib 18from six.moves import urllib
18import ujson as json 19import ujson as json
19 20
21from debtcollector import removals
22from oslo_config import cfg
23
20from monasca_notification.plugins import abstract_notifier 24from monasca_notification.plugins import abstract_notifier
21 25
26CONF = cfg.CONF
27
28
29def register_opts(conf):
30 gr = cfg.OptGroup(name='%s_notifier' % SlackNotifier.type)
31 opts = [
32 cfg.IntOpt(name='timeout', default=5, min=1),
33 cfg.BoolOpt(name='insecure', default=True),
34 cfg.StrOpt(name='ca_certs', default=None),
35 cfg.StrOpt(name='proxy', default=None)
36 ]
37
38 conf.register_group(gr)
39 conf.register_opts(opts, group=gr)
40
22 41
23class SlackNotifier(abstract_notifier.AbstractNotifier): 42class SlackNotifier(abstract_notifier.AbstractNotifier):
24 """This module is a notification plugin to integrate with Slack. 43 """This module is a notification plugin to integrate with Slack.
@@ -42,25 +61,24 @@ class SlackNotifier(abstract_notifier.AbstractNotifier):
42 https://api.slack.com/incoming-webhooks 61 https://api.slack.com/incoming-webhooks
43 """ 62 """
44 63
45 CONFIG_CA_CERTS = 'ca_certs' 64 type = 'slack'
46 CONFIG_INSECURE = 'insecure' 65
47 CONFIG_PROXY = 'proxy'
48 CONFIG_TIMEOUT = 'timeout'
49 MAX_CACHE_SIZE = 100 66 MAX_CACHE_SIZE = 100
50 RESPONSE_OK = 'ok' 67 RESPONSE_OK = 'ok'
51 68
52 _raw_data_url_caches = [] 69 _raw_data_url_caches = []
53 70
54 def __init__(self, log): 71 def __init__(self, log):
72 super(SlackNotifier, self).__init__()
55 self._log = log 73 self._log = log
56 74
75 @removals.remove(
76 message='Configuration of notifier is available through oslo.cfg',
77 version='1.9.0',
78 removal_version='3.0.0'
79 )
57 def config(self, config_dict): 80 def config(self, config_dict):
58 self._config = {'timeout': 5} 81 pass
59 self._config.update(config_dict)
60
61 @property
62 def type(self):
63 return "slack"
64 82
65 @property 83 @property
66 def statsd_name(self): 84 def statsd_name(self):
@@ -141,12 +159,12 @@ class SlackNotifier(abstract_notifier.AbstractNotifier):
141 159
142 # Default option is to do cert verification 160 # Default option is to do cert verification
143 # If ca_certs is specified, do cert validation and ignore insecure flag 161 # If ca_certs is specified, do cert validation and ignore insecure flag
144 verify = self._config.get(self.CONFIG_CA_CERTS, 162 verify = CONF.slack_notifier.ca_certs or not CONF.slack_notifier.insecure
145 (not self._config.get(self.CONFIG_INSECURE, True)))
146 163
147 proxyDict = None 164 proxy = CONF.slack_notifier.proxy
148 if (self.CONFIG_PROXY in self._config): 165 proxy_dict = None
149 proxyDict = {'https': self._config.get(self.CONFIG_PROXY)} 166 if proxy is not None:
167 proxy_dict = {'https': proxy}
150 168
151 data_format_list = ['json', 'data'] 169 data_format_list = ['json', 'data']
152 if url in SlackNotifier._raw_data_url_caches: 170 if url in SlackNotifier._raw_data_url_caches:
@@ -159,8 +177,8 @@ class SlackNotifier(abstract_notifier.AbstractNotifier):
159 'url': url, 177 'url': url,
160 'verify': verify, 178 'verify': verify,
161 'params': query_params, 179 'params': query_params,
162 'proxies': proxyDict, 180 'proxies': proxy_dict,
163 'timeout': self._config[self.CONFIG_TIMEOUT], 181 'timeout': CONF.slack_notifier.timeout,
164 data_format: slack_message 182 data_format: slack_message
165 } 183 }
166 if self._send_message(request_options): 184 if self._send_message(request_options):
diff --git a/monasca_notification/plugins/webhook_notifier.py b/monasca_notification/plugins/webhook_notifier.py
index fcf63c7..ac21040 100644
--- a/monasca_notification/plugins/webhook_notifier.py
+++ b/monasca_notification/plugins/webhook_notifier.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -16,20 +17,39 @@
16import requests 17import requests
17import ujson as json 18import ujson as json
18 19
20from debtcollector import removals
21from oslo_config import cfg
22
19from monasca_notification.plugins import abstract_notifier 23from monasca_notification.plugins import abstract_notifier
20 24
25CONF = cfg.CONF
26
27
28def register_opts(conf):
29 gr = cfg.OptGroup(name='%s_notifier' % WebhookNotifier.type)
30 opts = [
31 cfg.IntOpt(name='timeout', default=5, min=1)
32 ]
33
34 conf.register_group(gr)
35 conf.register_opts(opts, group=gr)
36
21 37
22class WebhookNotifier(abstract_notifier.AbstractNotifier): 38class WebhookNotifier(abstract_notifier.AbstractNotifier):
39
40 type = 'webhook'
41
23 def __init__(self, log): 42 def __init__(self, log):
43 super(WebhookNotifier, self).__init__()
24 self._log = log 44 self._log = log
25 45
46 @removals.remove(
47 message='Configuration of notifier is available through oslo.cfg',
48 version='1.9.0',
49 removal_version='3.0.0'
50 )
26 def config(self, config_dict): 51 def config(self, config_dict):
27 self._config = {'timeout': 5} 52 pass
28 self._config.update(config_dict)
29
30 @property
31 def type(self):
32 return "webhook"
33 53
34 @property 54 @property
35 def statsd_name(self): 55 def statsd_name(self):
@@ -60,7 +80,7 @@ class WebhookNotifier(abstract_notifier.AbstractNotifier):
60 result = requests.post(url=url, 80 result = requests.post(url=url,
61 data=json.dumps(body), 81 data=json.dumps(body),
62 headers=headers, 82 headers=headers,
63 timeout=self._config['timeout']) 83 timeout=CONF.webhook_notifier.timeout)
64 84
65 if result.status_code in range(200, 300): 85 if result.status_code in range(200, 300):
66 self._log.info("Notification successfully posted.") 86 self._log.info("Notification successfully posted.")
diff --git a/monasca_notification/processors/alarm_processor.py b/monasca_notification/processors/alarm_processor.py
index ae2b754..20ac84a 100644
--- a/monasca_notification/processors/alarm_processor.py
+++ b/monasca_notification/processors/alarm_processor.py
@@ -13,10 +13,11 @@
13# implied. 13# implied.
14# See the License for the specific language governing permissions and 14# See the License for the specific language governing permissions and
15# limitations under the License. 15# limitations under the License.
16import time
16 17
17import logging 18from oslo_config import cfg
19from oslo_log import log as logging
18import six 20import six
19import time
20import ujson as json 21import ujson as json
21 22
22from monasca_notification.common.repositories import exceptions as exc 23from monasca_notification.common.repositories import exceptions as exc
@@ -27,13 +28,14 @@ from monasca_notification import notification_exceptions
27 28
28 29
29log = logging.getLogger(__name__) 30log = logging.getLogger(__name__)
31CONF = cfg.CONF
30 32
31 33
32class AlarmProcessor(object): 34class AlarmProcessor(object):
33 def __init__(self, alarm_ttl, config): 35 def __init__(self):
34 self._alarm_ttl = alarm_ttl 36 self._alarm_ttl = CONF.alarm_processor.ttl
35 self._statsd = get_statsd_client(config) 37 self._statsd = get_statsd_client()
36 self._db_repo = get_db_repo(config) 38 self._db_repo = get_db_repo()
37 39
38 @staticmethod 40 @staticmethod
39 def _parse_alarm(alarm_data): 41 def _parse_alarm(alarm_data):
diff --git a/monasca_notification/processors/notification_processor.py b/monasca_notification/processors/notification_processor.py
index e757e4f..5cab47b 100644
--- a/monasca_notification/processors/notification_processor.py
+++ b/monasca_notification/processors/notification_processor.py
@@ -13,7 +13,7 @@
13# See the License for the specific language governing permissions and 13# See the License for the specific language governing permissions and
14# limitations under the License. 14# limitations under the License.
15 15
16import logging 16from oslo_log import log as logging
17 17
18from monasca_notification.common.repositories import exceptions as exc 18from monasca_notification.common.repositories import exceptions as exc
19from monasca_notification.common.utils import get_db_repo 19from monasca_notification.common.utils import get_db_repo
@@ -25,12 +25,14 @@ log = logging.getLogger(__name__)
25 25
26class NotificationProcessor(object): 26class NotificationProcessor(object):
27 27
28 def __init__(self, config): 28 def __init__(self):
29 self.statsd = get_statsd_client(config) 29 self.statsd = get_statsd_client()
30 notifiers.init(self.statsd) 30 notifiers.init(self.statsd)
31 notifiers.load_plugins(config['notification_types']) 31
32 notifiers.config(config['notification_types']) 32 notifiers.load_plugins()
33 self._db_repo = get_db_repo(config) 33 notifiers.config()
34
35 self._db_repo = get_db_repo()
34 self.insert_configured_plugins() 36 self.insert_configured_plugins()
35 37
36 def _remaining_plugin_types(self): 38 def _remaining_plugin_types(self):
diff --git a/monasca_notification/retry_engine.py b/monasca_notification/retry_engine.py
index 8c4aaca..38a4fc1 100644
--- a/monasca_notification/retry_engine.py
+++ b/monasca_notification/retry_engine.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015-2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -14,9 +15,11 @@
14# limitations under the License. 15# limitations under the License.
15 16
16import json 17import json
17import logging
18import time 18import time
19 19
20from oslo_config import cfg
21from oslo_log import log as logging
22
20from monasca_common.kafka import consumer 23from monasca_common.kafka import consumer
21from monasca_common.kafka import producer 24from monasca_common.kafka import producer
22from monasca_notification.common.utils import construct_notification_object 25from monasca_notification.common.utils import construct_notification_object
@@ -25,30 +28,24 @@ from monasca_notification.common.utils import get_statsd_client
25from monasca_notification.processors import notification_processor 28from monasca_notification.processors import notification_processor
26 29
27log = logging.getLogger(__name__) 30log = logging.getLogger(__name__)
31CONF = cfg.CONF
28 32
29 33
30class RetryEngine(object): 34class RetryEngine(object):
31 def __init__(self, config): 35 def __init__(self):
32 self._retry_interval = config['retry']['interval'] 36 self._statsd = get_statsd_client()
33 self._retry_max = config['retry']['max_attempts']
34
35 self._topics = {}
36 self._topics['notification_topic'] = config['kafka']['notification_topic']
37 self._topics['retry_topic'] = config['kafka']['notification_retry_topic']
38
39 self._statsd = get_statsd_client(config)
40 37
41 self._consumer = consumer.KafkaConsumer( 38 self._consumer = consumer.KafkaConsumer(
42 config['kafka']['url'], 39 CONF.kafka.url,
43 config['zookeeper']['url'], 40 ','.join(CONF.zookeeper.url),
44 config['zookeeper']['notification_retry_path'], 41 CONF.zookeeper.notification_retry_path,
45 config['kafka']['group'], 42 CONF.kafka.group,
46 config['kafka']['notification_retry_topic']) 43 CONF.kafka.notification_retry_topic
47 44 )
48 self._producer = producer.KafkaProducer(config['kafka']['url']) 45 self._producer = producer.KafkaProducer(CONF.kafka.url)
49 46
50 self._notifier = notification_processor.NotificationProcessor(config) 47 self._notifier = notification_processor.NotificationProcessor()
51 self._db_repo = get_db_repo(config) 48 self._db_repo = get_db_repo()
52 49
53 def run(self): 50 def run(self):
54 for raw_notification in self._consumer: 51 for raw_notification in self._consumer:
@@ -62,7 +59,7 @@ class RetryEngine(object):
62 self._consumer.commit() 59 self._consumer.commit()
63 continue 60 continue
64 61
65 wait_duration = self._retry_interval - ( 62 wait_duration = CONF.retry_engine.interval - (
66 time.time() - notification_data['notification_timestamp']) 63 time.time() - notification_data['notification_timestamp'])
67 64
68 if wait_duration > 0: 65 if wait_duration > 0:
@@ -71,19 +68,19 @@ class RetryEngine(object):
71 sent, failed = self._notifier.send([notification]) 68 sent, failed = self._notifier.send([notification])
72 69
73 if sent: 70 if sent:
74 self._producer.publish(self._topics['notification_topic'], 71 self._producer.publish(CONF.kafka.notification_topic,
75 [notification.to_json()]) 72 [notification.to_json()])
76 73
77 if failed: 74 if failed:
78 notification.retry_count += 1 75 notification.retry_count += 1
79 notification.notification_timestamp = time.time() 76 notification.notification_timestamp = time.time()
80 if notification.retry_count < self._retry_max: 77 if notification.retry_count < CONF.retry_engine.max_attempts:
81 log.error(u"retry failed for {} with name {} " 78 log.error(u"retry failed for {} with name {} "
82 u"at {}. " 79 u"at {}. "
83 u"Saving for later retry.".format(notification.type, 80 u"Saving for later retry.".format(notification.type,
84 notification.name, 81 notification.name,
85 notification.address)) 82 notification.address))
86 self._producer.publish(self._topics['retry_topic'], 83 self._producer.publish(CONF.kafka.notification_retry_topic,
87 [notification.to_json()]) 84 [notification.to_json()])
88 else: 85 else:
89 log.error(u"retry failed for {} with name {} " 86 log.error(u"retry failed for {} with name {} "
@@ -92,6 +89,6 @@ class RetryEngine(object):
92 .format(notification.type, 89 .format(notification.type,
93 notification.name, 90 notification.name,
94 notification.address, 91 notification.address,
95 self._retry_max)) 92 CONF.retry_engine.max_attempts))
96 93
97 self._consumer.commit() 94 self._consumer.commit()
diff --git a/monasca_notification/types/notifiers.py b/monasca_notification/types/notifiers.py
index a1040a1..89cebff 100644
--- a/monasca_notification/types/notifiers.py
+++ b/monasca_notification/types/notifiers.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -13,15 +14,19 @@
13# See the License for the specific language governing permissions and 14# See the License for the specific language governing permissions and
14# limitations under the License. 15# limitations under the License.
15 16
16import logging
17import time 17import time
18 18
19from monasca_common.simport import simport 19from debtcollector import removals
20from oslo_config import cfg
21from oslo_log import log as logging
22from oslo_utils import importutils
23
20from monasca_notification.plugins import email_notifier 24from monasca_notification.plugins import email_notifier
21from monasca_notification.plugins import pagerduty_notifier 25from monasca_notification.plugins import pagerduty_notifier
22from monasca_notification.plugins import webhook_notifier 26from monasca_notification.plugins import webhook_notifier
23 27
24log = logging.getLogger(__name__) 28log = logging.getLogger(__name__)
29CONF = cfg.CONF
25 30
26possible_notifiers = None 31possible_notifiers = None
27configured_notifiers = None 32configured_notifiers = None
@@ -49,14 +54,16 @@ def init(statsd_obj):
49 ] 54 ]
50 55
51 56
52def load_plugins(config): 57def load_plugins():
53 global possible_notifiers 58 global possible_notifiers
54 59 for plugin_class in CONF.notification_types.enabled:
55 for plugin_class in config.get("plugins", []):
56 try: 60 try:
57 possible_notifiers.append(simport.load(plugin_class)(log)) 61 plugin_class = plugin_class.replace(':', '.')
62 clz = importutils.import_class(plugin_class)
63 possible_notifiers.append(clz(logging.getLogger(plugin_class)))
58 except Exception: 64 except Exception:
59 log.exception("unable to load the class {0} , ignoring it".format(plugin_class)) 65 log.exception("unable to load the class %s , ignoring it" %
66 plugin_class)
60 67
61 68
62def enabled_notifications(): 69def enabled_notifications():
@@ -68,29 +75,20 @@ def enabled_notifications():
68 return results 75 return results
69 76
70 77
71def config(cfg): 78@removals.remove(
79 message='Loading the plugin configuration has been moved to oslo, '
80 'This method will be fully deleted in future releases',
81 version='1.9.0',
82 removal_version='3.0.0'
83)
84def config():
72 global possible_notifiers, configured_notifiers, statsd_counter 85 global possible_notifiers, configured_notifiers, statsd_counter
73 86
74 formatted_config = {t.lower(): v for t, v in cfg.items()}
75 for notifier in possible_notifiers: 87 for notifier in possible_notifiers:
76 ntype = notifier.type.lower() 88 ntype = notifier.type.lower()
77 if ntype in formatted_config: 89 configured_notifiers[ntype] = notifier
78 try: 90 statsd_counter[ntype] = statsd.get_counter(notifier.statsd_name)
79 notifier.config(formatted_config[ntype]) 91 log.info("{} notification ready".format(ntype))
80 configured_notifiers[ntype] = notifier
81 statsd_counter[ntype] = statsd.get_counter(notifier.statsd_name)
82 log.info("{} notification ready".format(ntype))
83 except Exception:
84 log.exception("config exception for {}".format(ntype))
85 else:
86 log.warn("No config data for type: {}".format(ntype))
87 config_with_no_notifiers = set(formatted_config.keys()) - set(configured_notifiers.keys())
88 # Plugins section contains only additional plugins and should not be
89 # considered as a separate plugin
90 if 'plugins' in config_with_no_notifiers:
91 config_with_no_notifiers.remove('plugins')
92 if config_with_no_notifiers:
93 log.warn("No notifiers found for {0}". format(", ".join(config_with_no_notifiers)))
94 92
95 93
96def send_notifications(notifications): 94def send_notifications(notifications):
diff --git a/monasca_notification/version.py b/monasca_notification/version.py
new file mode 100644
index 0000000..6c44d83
--- /dev/null
+++ b/monasca_notification/version.py
@@ -0,0 +1,23 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15from pbr import version
16
17__all__ = [
18 'version_info',
19 'version_string'
20]
21
22version_info = version.VersionInfo('monasca-notification')
23version_string = version_info.version_string()
diff --git a/requirements.txt b/requirements.txt
index e34b634..63dd412 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,12 @@
1# The order of packages is significant, because pip processes them in the order 1# The order of packages is significant, because pip processes them in the order
2# of appearance. Changing the order has an impact on the overall integration 2# of appearance. Changing the order has an impact on the overall integration
3# process, which may cause wedges in the gate later. 3# process, which may cause wedges in the gate later.
4pbr!=2.1.0,>=2.0.0 # Apache-2.0 4pbr>=2.0.0,!=2.1.0 # Apache-2.0
5debtcollector>=1.2.0 # Apache-2.0
5monasca-statsd>=1.1.0 # Apache-2.0 6monasca-statsd>=1.1.0 # Apache-2.0
6requests>=2.14.2 # Apache-2.0 7requests>=2.14.2 # Apache-2.0
7PyYAML>=3.10.0 # MIT 8PyYAML>=3.10 # MIT
8six>=1.9.0 # MIT 9six>=1.9.0 # MIT
9monasca-common>=1.4.0 # Apache-2.0 10monasca-common>=1.4.0 # Apache-2.0
11oslo.config>=4.6.0 # Apache-2.0
12oslo.log>=3.30.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 268218f..d40653c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -13,8 +13,10 @@ home-page = https://github.com/stackforge/monasca-notification
13license = Apache 13license = Apache
14 14
15[entry_points] 15[entry_points]
16console_scripts = 16console_scripts =
17 monasca-notification = monasca_notification.main:main 17 monasca-notification = monasca_notification.main:main
18oslo.config.opts =
19 monasca_notification = monasca_notification.conf:list_opts
18 20
19[files] 21[files]
20packages = monasca_notification 22packages = monasca_notification
diff --git a/test-requirements.txt b/test-requirements.txt
index c8b2828..b5fb958 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -2,14 +2,16 @@
2# of appearance. Changing the order has an impact on the overall integration 2# of appearance. Changing the order has an impact on the overall integration
3# process, which may cause wedges in the gate later. 3# process, which may cause wedges in the gate later.
4# Hacking already pins down pep8, pyflakes and flake8 4# Hacking already pins down pep8, pyflakes and flake8
5
5bandit>=1.1.0 # Apache-2.0 6bandit>=1.1.0 # Apache-2.0
7Babel>=2.3.4,!=2.4.0 # BSD
6hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0 8hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
7coverage!=4.4,>=4.0 # Apache-2.0 9coverage>=4.0,!=4.4 # Apache-2.0
8mock>=2.0 # BSD 10mock>=2.0.0 # BSD
9funcsigs>=0.4;python_version=='2.7' or python_version=='2.6' # Apache-2.0 11funcsigs>=1.0.0;python_version=='2.7' or python_version=='2.6' # Apache-2.0
10os-testr>=0.8.0 # Apache-2.0 12os-testr>=1.0.0 # Apache-2.0
11oslotest>=1.10.0 # Apache-2.0 13oslotest>=1.10.0 # Apache-2.0
12testrepository>=0.0.18 # Apache-2.0/BSD 14testrepository>=0.0.18 # Apache-2.0/BSD
13SQLAlchemy!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8,>=1.0.10 # MIT 15SQLAlchemy>=1.0.10,!=1.1.5,!=1.1.6,!=1.1.7,!=1.1.8 # MIT
14PyMySQL>=0.7.6 # MIT License 16PyMySQL>=0.7.6 # MIT License
15psycopg2>=2.5 # LGPL/ZPL 17psycopg2>=2.6.2 # LGPL/ZPL
diff --git a/tests/base.py b/tests/base.py
new file mode 100644
index 0000000..f39572a
--- /dev/null
+++ b/tests/base.py
@@ -0,0 +1,83 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import fixtures
16import mock
17
18from oslo_config import cfg
19from oslo_config import fixture as oo_cfg
20from oslotest import base as oslotest_base
21
22from monasca_notification import conf
23from monasca_notification import config
24
25
26class DisableStatsdFixture(fixtures.Fixture):
27
28 def setUp(self):
29 super(DisableStatsdFixture, self).setUp()
30 statsd_patch = mock.patch('monascastatsd.Connection')
31 statsd_patch.start()
32 self.addCleanup(statsd_patch.stop)
33
34
35class ConfigFixture(oo_cfg.Config):
36 """Mocks configuration"""
37
38 def __init__(self):
39 super(ConfigFixture, self).__init__(config.CONF)
40
41 def setUp(self):
42 super(ConfigFixture, self).setUp()
43
44 self.addCleanup(self._clean_config_loaded_flag)
45
46 conf.register_opts()
47 # prevent test from trying to load the yaml file
48 config.parse_args(argv=[], no_yaml=True)
49
50 @staticmethod
51 def _clean_config_loaded_flag():
52 config._CONF_LOADED = False
53
54
55class BaseTestCase(oslotest_base.BaseTestCase):
56
57 def setUp(self):
58 super(BaseTestCase, self).setUp()
59 self.useFixture(ConfigFixture())
60 self.useFixture(DisableStatsdFixture())
61
62 @staticmethod
63 def conf_override(**kw):
64 """Override flag variables for a test."""
65 group = kw.pop('group', None)
66 for k, v in kw.items():
67 cfg.CONF.set_override(k, v, group)
68
69 @staticmethod
70 def conf_default(**kw):
71 """Override flag variables for a test."""
72 group = kw.pop('group', None)
73 for k, v in kw.items():
74 cfg.CONF.set_default(k, v, group)
75
76
77class PluginTestCase(BaseTestCase):
78 register_opts = None
79
80 def setUp(self, register_opts=None):
81 super(PluginTestCase, self).setUp()
82 if register_opts:
83 register_opts(conf.CONF)
diff --git a/tests/resources/notification.yaml b/tests/resources/notification.yaml
new file mode 100644
index 0000000..cbc6a1f
--- /dev/null
+++ b/tests/resources/notification.yaml
@@ -0,0 +1,125 @@
1kafka:
2 url: 127.0.0.1:9092
3 group: a
4 alarm_topic: b
5 notification_topic: c
6 notification_retry_topic: d
7 periodic:
8 60: e
9 max_offset_lag: 666
10
11database:
12 repo_driver: mysql
13 orm:
14 url: 'postgres://a:b@127.0.0.1:9999/goo'
15
16mysql:
17 host: 100.99.100.99
18 port: 3306
19 user: goku
20 passwd: kame-ha-me-ha
21 db: planet_vegeta
22
23postgresql:
24 user: goku
25 password: kame-ha-me-ha
26 database: planet_vegeta
27 port: 9999
28 host: 100.10.100.10
29
30notification_types:
31 plugins:
32 - monasca_notification.plugins.hipchat_notifier:HipChatNotifier
33 - monasca_notification.plugins.slack_notifier:SlackNotifier
34 - monasca_notification.plugins.jira_notifier:JiraNotifier
35
36 email:
37 server: 127.0.0.1
38 port: 25
39 user:
40 password:
41 timeout: 60
42 from_addr: root@localhost
43 grafana_url: 'http://127.0.0.1:3000'
44
45 webhook:
46 timeout: 123
47
48 pagerduty:
49 timeout: 231
50 url: "https://a.b.c/d/e/f.json"
51
52 hipchat:
53 timeout: 512
54 ca_certs: "/a.crt"
55 insecure: True
56 proxy: https://myproxy.corp.invalid:9999
57
58 slack:
59 timeout: 512
60 ca_certs: "/a.crt"
61 insecure: True
62 proxy: https://myproxy.corp.invalid:9999
63
64 jira:
65 user: username
66 password: password
67 timeout: 666
68 custom_formatter: /some_yml.yml
69 proxy: www.example.org
70
71processors:
72 alarm:
73 number: 666
74 ttl: 666
75 notification:
76 number: 666
77
78retry:
79 interval: 300
80 max_attempts: 500
81
82queues:
83 alarms_size: 1024
84 finished_size: 1024
85 notifications_size: 1024
86 sent_notifications_size: 1024
87
88zookeeper:
89 url: 127.0.0.1:2181
90 notification_path: /foo/bar
91 notification_retry_path: /son/goku
92 periodic_path:
93 666: /bu/666_bubu
94
95logging:
96 raise_exceptions: False
97 version: 1
98 disable_existing_loggers: False
99 formatters:
100 default:
101 format: "%(asctime)s %(levelname)s %(name)s %(message)s"
102 handlers:
103 console:
104 class: logging.StreamHandler
105 formatter: default
106 file:
107 class : logging.handlers.RotatingFileHandler
108 filename: /tmp/notification.log
109 formatter: default
110 maxBytes: 10485760 # Rotate at file size ~10MB
111 backupCount: 5 # Keep 5 older logs around
112 loggers:
113 kazoo:
114 level: WARN
115 kafka:
116 level: WARN
117 statsd:
118 level: WARN
119 root:
120 handlers:
121 - console
122 level: DEBUG
123statsd:
124 host: 'localhost'
125 port: 8125
diff --git a/tests/test_alarm_processor.py b/tests/test_alarm_processor.py
index e6f32ae..c05691b 100644
--- a/tests/test_alarm_processor.py
+++ b/tests/test_alarm_processor.py
@@ -1,4 +1,5 @@
1# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP 1# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
2# Copyright 2017 Fujitsu LIMITED
2# 3#
3# Licensed under the Apache License, Version 2.0 (the "License"); 4# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License. 5# you may not use this file except in compliance with the License.
@@ -19,17 +20,19 @@ import collections
19import json 20import json
20import mock 21import mock
21import time 22import time
22import unittest
23 23
24from monasca_notification import notification as m_notification 24from monasca_notification import notification as m_notification
25from monasca_notification.processors import alarm_processor 25from monasca_notification.processors import alarm_processor
26 26
27from tests import base
28
27alarm_tuple = collections.namedtuple('alarm_tuple', ['offset', 'message']) 29alarm_tuple = collections.namedtuple('alarm_tuple', ['offset', 'message'])
28message_tuple = collections.namedtuple('message_tuple', ['value']) 30message_tuple = collections.namedtuple('message_tuple', ['value'])
29 31
30 32
31class TestAlarmProcessor(unittest.TestCase): 33class TestAlarmProcessor(base.BaseTestCase):
32 def setUp(self): 34 def setUp(self):
35 super(TestAlarmProcessor, self).setUp()
33 self.trap = [] 36 self.trap = []
34 37
35 def _create_raw_alarm(self, partition, offset, message): 38 def _create_raw_alarm(self, partition, offset, message):
@@ -55,16 +58,13 @@ class TestAlarmProcessor(unittest.TestCase):
55 mock_mysql.cursor.return_value = mock_mysql 58 mock_mysql.cursor.return_value = mock_mysql
56 mock_mysql.__iter__.return_value = sql_response 59 mock_mysql.__iter__.return_value = sql_response
57 60
58 config = {'mysql': {'ssl': None, 61 self.conf_override(group='mysql', ssl=None,
59 'host': 'mysql_host', 62 host='localhost', port='3306',
60 'port': 'mysql_port', 63 user='mysql_user', db='dbname',
61 'user': 'mysql_user', 64 passwd='mysql_passwd')
62 'db': 'dbname', 65 self.conf_override(group='statsd', host='localhost',
63 'passwd': 'mysql_passwd'}, 66 port=8125)
64 'statsd': {'host': 'localhost', 67 processor = alarm_processor.AlarmProcessor()
65 'port': 8125}}
66
67 processor = alarm_processor.AlarmProcessor(600, config)
68 68
69 return processor.to_notification(alarm) 69 return processor.to_notification(alarm)
70 70
diff --git a/tests/test_config.py b/tests/test_config.py
new file mode 100644
index 0000000..1d072b2
--- /dev/null
+++ b/tests/test_config.py
@@ -0,0 +1,226 @@
1# Copyright 2017 FUJITSU LIMITED
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import mock
16from oslo_config import cfg
17from oslo_utils import importutils
18import yaml
19
20from monasca_notification import conf
21from monasca_notification import config
22from tests import base
23
24
25class TestConfig(base.BaseTestCase):
26 @mock.patch('monasca_notification.config.conf')
27 def test_should_load_deprecated_yaml(self, conf):
28 fake_config = """
29 sayians:
30 - goku
31 - vegeta
32 """
33 yaml_config = self.create_tempfiles(
34 files=[('notification', fake_config)],
35 ext='.yml'
36 )[0]
37
38 self.conf_override(yaml_config=yaml_config)
39
40 config.set_from_yaml()
41
42 fake_yaml_config = {
43 'sayians': ['goku', 'vegeta']
44 }
45 conf.load_from_yaml.assert_called_once_with(fake_yaml_config)
46
47 @mock.patch('monasca_notification.config.conf')
48 def test_should_not_load_deprecated_yaml(self, conf):
49 config.set_from_yaml()
50 conf.load_from_yaml.assert_not_called()
51
52
53class TestYamlOverriding(base.BaseTestCase):
54 # TOP_LEVEL keys represents old groups in YAML file
55 VERIFIERS = {
56 'statsd': {
57 'groups': [
58 ('statsd', {
59 'host': 'localhost',
60 'port': 8125
61 })
62 ]
63 },
64 'retry': {
65 'groups': [
66 ('retry_engine', {
67 'interval': 300,
68 'max_attempts': 500
69 })
70 ]
71 },
72 'queues': {
73 'groups': [
74 ('queues', {
75 'alarms_size': 1024,
76 'finished_size': 1024,
77 'notifications_size': 1024,
78 'sent_notifications_size': 1024
79 })
80 ]
81 },
82 'zookeeper': {
83 'groups': [
84 ('zookeeper', {
85 'url': ['127.0.0.1:2181'],
86 'notification_path': '/foo/bar',
87 'periodic_path': {
88 666: '/bu/666_bubu'
89 },
90 })
91 ]
92 },
93 'kafka': {
94 'groups': [
95 ('kafka', {
96 'url': ['127.0.0.1:9092'],
97 'group': 'a',
98 'alarm_topic': 'b',
99 'notification_topic': 'c',
100 'notification_retry_topic': 'd',
101 'periodic': {
102 60: 'e'
103 },
104 'max_offset_lag': 666
105 })
106 ]
107 },
108 'processors': {
109 'groups': [
110 ('alarm_processor', {'number': 666, 'ttl': 666}),
111 ('notification_processor', {'number': 666})
112 ]
113 },
114 'postgresql': {
115 'groups': [
116 ('postgresql', {
117 'host': '100.10.100.10',
118 'port': 9999,
119 'user': 'goku',
120 'password': 'kame-ha-me-ha',
121 'database': 'planet_vegeta'
122 })
123 ]
124 },
125 'mysql': {
126 'groups': [
127 ('mysql', {
128 'host': '100.99.100.99',
129 'port': 3306,
130 'user': 'goku',
131 'passwd': 'kame-ha-me-ha',
132 'db': 'planet_vegeta',
133 'ssl': {}
134 })
135 ]
136 },
137 'database': {
138 'groups': [
139 ('database', {'repo_driver': importutils.import_class(
140 'monasca_notification.common.repositories.mysql.'
141 'mysql_repo.MysqlRepo')}),
142 ('orm', {'url': 'postgres://a:b@127.0.0.1:9999/goo'})
143 ]
144 },
145 'notification_types': {
146 'groups': [
147 ('notification_types', {
148 'enabled': [
149 'monasca_notification.plugins.hipchat_notifier:HipChatNotifier',
150 'monasca_notification.plugins.slack_notifier:SlackNotifier',
151 'monasca_notification.plugins.jira_notifier:JiraNotifier',
152 'monasca_notification.plugins.email_notifier:EmailNotifier',
153 'monasca_notification.plugins.pagerduty_notifier:PagerdutyNotifier',
154 'monasca_notification.plugins.webhook_notifier:WebhookNotifier',
155 ]
156 }),
157 ('email_notifier', {
158 'server': '127.0.0.1',
159 'port': 25,
160 'user': None,
161 'password': None,
162 'timeout': 60,
163 'from_addr': 'root@localhost',
164 'grafana_url': 'http://127.0.0.1:3000'
165 }),
166 ('webhook_notifier', {'timeout': 123}),
167 ('pagerduty_notifier', {
168 'timeout': 231,
169 'url': 'https://a.b.c/d/e/f.json'
170 }),
171 ('hipchat_notifier', {
172 'timeout': 512,
173 'ca_certs': "/a.crt",
174 'insecure': True,
175 'proxy': 'https://myproxy.corp.invalid:9999'
176 }),
177 ('slack_notifier', {
178 'timeout': 512,
179 'ca_certs': "/a.crt",
180 'insecure': True,
181 'proxy': 'https://myproxy.corp.invalid:9999'
182 }),
183 ('jira_notifier', {
184 'user': 'username',
185 'password': 'password',
186 'timeout': 666,
187 'custom_formatter': '/some_yml.yml',
188 'proxy': 'www.example.org'
189 })
190 ]
191 }
192 }
193
194 def setUp(self):
195 super(TestYamlOverriding, self).setUp()
196 self.yaml_config = yaml.safe_load(
197 open('tests/resources/notification.yaml', 'rb')
198 )
199
200 def test_overriding(self):
201
202 conf.load_from_yaml(yaml_config=self.yaml_config, conf=config.CONF)
203 opts = config.CONF
204
205 for group in self.VERIFIERS.keys():
206 verifier_details = self.VERIFIERS[group]
207 groups = verifier_details['groups']
208
209 for opt_group, opt_values in groups:
210
211 for key, value in opt_values.items():
212 try:
213 opt_value = opts[opt_group][key]
214 except (cfg.NoSuchOptError, cfg.NoSuchGroupError) as ex:
215 self.fail(str(ex))
216 else:
217 msg = ('%s not overridden in group %s'
218 % (key, opt_group))
219
220 if (isinstance(value, list) and
221 isinstance(opt_value, list)):
222 for v in value:
223 self.assertIn(v, opt_value, msg)
224 continue
225
226 self.assertEqual(value, opt_value, msg)
diff --git a/tests/test_email_notification.py b/tests/test_email_notification.py
index baef179..c5b1342 100644
--- a/tests/test_email_notification.py
+++ b/tests/test_email_notification.py
@@ -1,5 +1,6 @@
1# coding=utf-8 1# coding=utf-8
2# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP 2# (C) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
3# Copyright 2017 Fujitsu LIMITED
3# 4#
4# Licensed under the Apache License, Version 2.0 (the "License"); 5# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License. 6# you may not use this file except in compliance with the License.
@@ -20,7 +21,6 @@ import mock
20import smtplib 21import smtplib
21import socket 22import socket
22import time 23import time
23import unittest
24 24
25import six 25import six
26 26
@@ -34,6 +34,7 @@ else:
34 34
35from monasca_notification.notification import Notification 35from monasca_notification.notification import Notification
36from monasca_notification.plugins import email_notifier 36from monasca_notification.plugins import email_notifier
37from tests import base
37 38
38UNICODE_CHAR = six.unichr(2344) 39UNICODE_CHAR = six.unichr(2344)
39UNICODE_CHAR_ENCODED = UNICODE_CHAR.encode("utf-8") 40UNICODE_CHAR_ENCODED = UNICODE_CHAR.encode("utf-8")
@@ -113,20 +114,16 @@ class smtpStubException(object):
113 raise smtplib.SMTPServerDisconnected 114 raise smtplib.SMTPServerDisconnected
114 115
115 116
116class TestEmail(unittest.TestCase): 117class TestEmail(base.PluginTestCase):
117 def setUp(self): 118 def setUp(self):
118 self.trap = [] 119 super(TestEmail, self).setUp(email_notifier.register_opts)
119
120 self.email_config = {'server': 'my.smtp.server',
121 'port': 25,
122 'user': None,
123 'password': None,
124 'timeout': 60,
125 'from_addr': 'hpcs.mon@hp.com',
126 'grafana_url': 'http://127.0.0.1:3000'}
127 120
128 def tearDown(self): 121 self.trap = []
129 pass 122 self.conf_override(group='email_notifier', server='my.smtp.server',
123 port=25, user=None,
124 password=None, timeout=60,
125 from_addr='hpcs.mon@hp.com',
126 grafana_url='http://127.0.0.1:3000')
130 127
131 def _smtpStub(self, *arg, **kwargs): 128 def _smtpStub(self, *arg, **kwargs):
132 return smtpStub(self.trap) 129 return smtpStub(self.trap)
@@ -143,7 +140,6 @@ class TestEmail(unittest.TestCase):
143 mock_log.error = self.trap.append 140 mock_log.error = self.trap.append
144 141
145 email = email_notifier.EmailNotifier(mock_log) 142 email = email_notifier.EmailNotifier(mock_log)
146 email.config(self.email_config)
147 143
148 alarm_dict = alarm(metric) 144 alarm_dict = alarm(metric)
149 145
@@ -269,7 +265,7 @@ class TestEmail(unittest.TestCase):
269 265
270 email = email_notifier.EmailNotifier(mock_log) 266 email = email_notifier.EmailNotifier(mock_log)
271 267
272 email.config(self.email_config) 268 email.config()
273 269
274 alarm_dict = alarm(metrics) 270 alarm_dict = alarm(metrics)
275 271
@@ -313,7 +309,7 @@ class TestEmail(unittest.TestCase):
313 309
314 email = email_notifier.EmailNotifier(mock_log) 310 email = email_notifier.EmailNotifier(mock_log)
315 311
316 email.config(self.email_config) 312 email.config()
317 313
318 del self.trap[:] 314 del self.trap[:]
319 315
@@ -324,8 +320,7 @@ class TestEmail(unittest.TestCase):
324 email_result = email.send_notification(notification) 320 email_result = email.send_notification(notification)
325 321
326 self.assertFalse(email_result) 322 self.assertFalse(email_result)
327 self.assertIn("Connecting to Email Server {}" 323 self.assertIn("Connecting to Email Server my.smtp.server",
328 .format(self.email_config['server']),
329 self.trap) 324 self.trap)
330 325
331 @mock.patch('monasca_notification.plugins.email_notifier.smtplib') 326 @mock.patch('monasca_notification.plugins.email_notifier.smtplib')
@@ -358,7 +353,7 @@ class TestEmail(unittest.TestCase):
358 353
359 email = email_notifier.EmailNotifier(mock_log) 354 email = email_notifier.EmailNotifier(mock_log)
360 355
361 email.config(self.email_config) 356 email.config()
362 357
363 alarm_dict = alarm(metrics) 358 alarm_dict = alarm(metrics)
364 359
@@ -398,7 +393,7 @@ class TestEmail(unittest.TestCase):
398 393
399 email = email_notifier.EmailNotifier(mock_log) 394 email = email_notifier.EmailNotifier(mock_log)
400 395
401 email.config(self.email_config) 396 email.config()
402 397
403 alarm_dict = alarm(metrics) 398 alarm_dict = alarm(metrics)
404 399
@@ -438,7 +433,7 @@ class TestEmail(unittest.TestCase):
438 433
439 email = email_notifier.EmailNotifier(mock_log) 434 email = email_notifier.EmailNotifier(mock_log)
440 435
441 email.config(self.email_config) 436 email.config()
442 437
443 alarm_dict = alarm(metrics) 438 alarm_dict = alarm(metrics)
444 439
@@ -471,7 +466,6 @@ class TestEmail(unittest.TestCase):
471 mock_smtp.SMTPException = smtplib.SMTPException 466 mock_smtp.SMTPException = smtplib.SMTPException
472 467
473 email = email_notifier.EmailNotifier(mock_log) 468 email = email_notifier.EmailNotifier(mock_log)
474 email.config(self.email_config)
475 469
476 # Create alarm timestamp and timestamp for 'from' and 'to' dates in milliseconds. 470 # Create alarm timestamp and timestamp for 'from' and 'to' dates in milliseconds.
477 alarm_date = datetime.datetime(2017, 6, 7, 18, 0) 471 alarm_date = datetime.datetime(2017, 6, 7, 18, 0)
diff --git a/tests/test_hipchat_notification.py b/tests/test_hipchat_notification.py
index a9500e7..b5ef254 100644
--- a/tests/test_hipchat_notification.py
+++ b/tests/test_hipchat_notification.py
@@ -13,12 +13,12 @@
13 13
14import json 14import json
15import mock 15import mock
16import unittest
17 16
18import six 17import six
19 18
20from monasca_notification import notification as m_notification 19from monasca_notification import notification as m_notification
21from monasca_notification.plugins import hipchat_notifier 20from monasca_notification.plugins import hipchat_notifier
21from tests import base
22 22
23if six.PY2: 23if six.PY2:
24 import Queue as queue 24 import Queue as queue
@@ -47,12 +47,15 @@ class requestsResponse(object):
47 self.status_code = status 47 self.status_code = status
48 48
49 49
50class TestHipchat(unittest.TestCase): 50class TestHipchat(base.PluginTestCase):
51 def setUp(self): 51 def setUp(self):
52 super(TestHipchat, self).setUp(hipchat_notifier.register_opts)
53 self.conf_default(group='hipchat_notifier', timeout=50)
54
52 self.trap = queue.Queue() 55 self.trap = queue.Queue()
53 self.hipchat_config = {'timeout': 50}
54 56
55 def tearDown(self): 57 def tearDown(self):
58 super(TestHipchat, self).tearDown()
56 self.assertTrue(self.trap.empty()) 59 self.assertTrue(self.trap.empty())
57 60
58 def _http_post_200(self, url, data, **kwargs): 61 def _http_post_200(self, url, data, **kwargs):
@@ -72,7 +75,7 @@ class TestHipchat(unittest.TestCase):
72 75
73 hipchat = hipchat_notifier.HipChatNotifier(mock_log) 76 hipchat = hipchat_notifier.HipChatNotifier(mock_log)
74 77
75 hipchat.config(self.hipchat_config) 78 hipchat.config()
76 79
77 metric = [] 80 metric = []
78 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}} 81 metric_data = {'dimensions': {'hostname': 'foo1', 'service': 'bar1'}}
diff --git a/tests/test_jira_notification.py b/tests/test_jira_notification.py
index 7f14ae5..58d7587 100644
--- a/