00cc2527e3
Includes missing configuration options for notifiers in generated config file. Change-Id: I06e6d51bca3b180768bc8c71ff10c9f6e69c5e8b Story: #2003793 Task: #26506
208 lines
7.7 KiB
Python
208 lines
7.7 KiB
Python
# (C) Copyright 2016-2017 Hewlett Packard Enterprise Development LP
|
|
# Copyright 2017 Fujitsu LIMITED
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import requests
|
|
from six.moves import urllib
|
|
import ujson as json
|
|
|
|
from debtcollector import removals
|
|
from oslo_config import cfg
|
|
|
|
from monasca_notification.plugins import abstract_notifier
|
|
|
|
CONF = cfg.CONF
|
|
|
|
|
|
class SlackNotifier(abstract_notifier.AbstractNotifier):
|
|
"""This module is a notification plugin to integrate with Slack.
|
|
|
|
This plugin supports 2 types of APIs below.
|
|
|
|
1st: Slack API
|
|
notification.address = https://slack.com/api/chat.postMessage?token={token}&channel=#foobar
|
|
|
|
You need to specify your token and channel name in the address.
|
|
Regarding {token}, login to your slack account via browser and check the following page.
|
|
https://api.slack.com/docs/oauth-test-tokens
|
|
|
|
2nd: Incoming webhook
|
|
notification.address = https://hooks.slack.com/services/foo/bar/buz
|
|
|
|
You need to get the Incoming webhook URL.
|
|
Login to your slack account via browser and check the following page.
|
|
https://my.slack.com/services/new/incoming-webhook/
|
|
Slack document about incoming webhook:
|
|
https://api.slack.com/incoming-webhooks
|
|
"""
|
|
|
|
type = 'slack'
|
|
|
|
MAX_CACHE_SIZE = 100
|
|
RESPONSE_OK = 'ok'
|
|
|
|
_raw_data_url_caches = []
|
|
|
|
def __init__(self, log):
|
|
super(SlackNotifier, self).__init__()
|
|
self._log = log
|
|
|
|
@removals.remove(
|
|
message='Configuration of notifier is available through oslo.cfg',
|
|
version='1.9.0',
|
|
removal_version='3.0.0'
|
|
)
|
|
def config(self, config_dict):
|
|
pass
|
|
|
|
@property
|
|
def statsd_name(self):
|
|
return 'sent_slack_count'
|
|
|
|
def _build_slack_message(self, notification):
|
|
"""Builds slack message body
|
|
"""
|
|
body = {'alarm_id': notification.alarm_id,
|
|
'alarm_definition_id': notification.raw_alarm['alarmDefinitionId'],
|
|
'alarm_name': notification.alarm_name,
|
|
'alarm_description': notification.raw_alarm['alarmDescription'],
|
|
'alarm_timestamp': notification.alarm_timestamp,
|
|
'state': notification.state,
|
|
'old_state': notification.raw_alarm['oldState'],
|
|
'message': notification.message,
|
|
'tenant_id': notification.tenant_id,
|
|
'metrics': notification.metrics}
|
|
|
|
slack_request = {}
|
|
slack_request['text'] = json.dumps(body, indent=3)
|
|
|
|
return slack_request
|
|
|
|
def _check_response(self, result):
|
|
if 'application/json' in result.headers.get('Content-Type'):
|
|
response = result.json()
|
|
if response.get(self.RESPONSE_OK):
|
|
return True
|
|
else:
|
|
self._log.error('Received an error message when trying to send to slack. error={}'
|
|
.format(response.get('error')))
|
|
return False
|
|
elif self.RESPONSE_OK == result.text:
|
|
return True
|
|
else:
|
|
self._log.error('Received an error message when trying to send to slack. error={}'
|
|
.format(result.text))
|
|
return False
|
|
|
|
def _send_message(self, request_options):
|
|
try:
|
|
url = request_options.get('url')
|
|
result = requests.post(**request_options)
|
|
if result.status_code not in range(200, 300):
|
|
self._log.error('Received an HTTP code {} when trying to post on URL {}.'
|
|
.format(result.status_code, url))
|
|
return False
|
|
|
|
# Slack returns 200 ok even if the token is invalid. Response has valid error message
|
|
if self._check_response(result):
|
|
self._log.info('Notification successfully posted.')
|
|
return True
|
|
|
|
self._log.error('Failed to send to slack on URL {}.'.format(url))
|
|
return False
|
|
except Exception as err:
|
|
self._log.exception('Error trying to send to slack on URL {}. Detail: {}'
|
|
.format(url, err))
|
|
return False
|
|
|
|
def send_notification(self, notification):
|
|
"""Send the notification via slack
|
|
Posts on the given url
|
|
"""
|
|
|
|
slack_message = self._build_slack_message(notification)
|
|
|
|
address = notification.address
|
|
# '#' is reserved character and replace it with ascii equivalent
|
|
# Slack room has '#' as first character
|
|
address = address.replace('#', '%23')
|
|
|
|
parsed_url = urllib.parse.urlsplit(address)
|
|
query_params = urllib.parse.parse_qs(parsed_url.query)
|
|
# URL without query params
|
|
url = urllib.parse.urljoin(address, urllib.parse.urlparse(address).path)
|
|
|
|
# Default option is to do cert verification
|
|
# If ca_certs is specified, do cert validation and ignore insecure flag
|
|
verify = CONF.slack_notifier.ca_certs or not CONF.slack_notifier.insecure
|
|
|
|
proxy = CONF.slack_notifier.proxy
|
|
proxy_dict = None
|
|
if proxy is not None:
|
|
proxy_dict = {'https': proxy}
|
|
|
|
data_format_list = ['json', 'data']
|
|
if url in SlackNotifier._raw_data_url_caches:
|
|
data_format_list = ['data']
|
|
|
|
for data_format in data_format_list:
|
|
self._log.info('Trying to send message to {} as {}'
|
|
.format(url, data_format))
|
|
request_options = {
|
|
'url': url,
|
|
'verify': verify,
|
|
'params': query_params,
|
|
'proxies': proxy_dict,
|
|
'timeout': CONF.slack_notifier.timeout,
|
|
data_format: slack_message
|
|
}
|
|
if self._send_message(request_options):
|
|
if (data_format == 'data' and
|
|
url not in SlackNotifier._raw_data_url_caches and
|
|
len(SlackNotifier._raw_data_url_caches) < self.MAX_CACHE_SIZE):
|
|
# NOTE:
|
|
# There are a few URLs which can accept only raw data, so
|
|
# only the URLs with raw data are kept in the cache. When
|
|
# too many URLs exists, it can be considered malicious
|
|
# user registers them.
|
|
# In this case, older ones should be safer than newer
|
|
# ones. When exceeding the cache size, do not replace the
|
|
# the old cache with the newer one.
|
|
SlackNotifier._raw_data_url_caches.append(url)
|
|
return True
|
|
|
|
self._log.info('Failed to send message to {} as {}'
|
|
.format(url, data_format))
|
|
return False
|
|
|
|
slack_notifier_group = cfg.OptGroup(name='%s_notifier' % SlackNotifier.type)
|
|
slack_notifier_opts = [
|
|
cfg.IntOpt(name='timeout', default=5, min=1),
|
|
cfg.BoolOpt(name='insecure', default=True),
|
|
cfg.StrOpt(name='ca_certs', default=None),
|
|
cfg.StrOpt(name='proxy', default=None)
|
|
]
|
|
|
|
|
|
def register_opts(conf):
|
|
conf.register_group(slack_notifier_group)
|
|
conf.register_opts(slack_notifier_opts, group=slack_notifier_group)
|
|
|
|
|
|
def list_opts():
|
|
return {
|
|
slack_notifier_group: slack_notifier_opts
|
|
}
|