Implement a basic alarm notification service

Change-Id: I74b28a8754a9541981ed8e8f27cd05ceb43c3776
Blueprint: alarm-notifier
This commit is contained in:
Julien Danjou 2013-06-21 17:03:16 +02:00
parent ced2b691c1
commit 2093c1cb56
6 changed files with 270 additions and 1 deletions

View File

@ -0,0 +1,35 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance <licensing@enovance.com>
#
# Author: Julien Danjou <julien@danjou.info>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import abc
class AlarmNotifier(object):
"""Base class for alarm notifier plugins."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def notify(self, action, alarm, state, reason):
"""Notify that an alarm has been triggered.
:param action: The action that is being attended, as a parsed URL.
:param alarm: The triggered alarm.
:param state: The state the alarm is now in.
:param reason: The reason the alarm changed its state.
"""

View File

@ -0,0 +1,31 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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.
"""Log alarm notifier."""
from ceilometer.alarm import notifier
from ceilometer.openstack.common import log
LOG = log.getLogger(__name__)
class LogAlarmNotifier(notifier.AlarmNotifier):
"Log alarm notifier."""
def notify(action, alarm, state, reason):
LOG.info("Notifying alarm %s in state %s with action %s because %s",
alarm, state, action, reason)

View File

@ -0,0 +1,30 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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.
"""Test alarm notifier."""
from ceilometer.alarm import notifier
class TestAlarmNotifier(notifier.AlarmNotifier):
"Test alarm notifier."""
def __init__(self):
self.notifications = []
def notify(self, action, alarm, state, reason):
self.notifications.append((action, alarm, state, reason))

View File

@ -1,8 +1,10 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Red Hat, Inc
# Copyright © 2013 eNovance <licensing@enovance.com>
#
# Author: Eoghan Glynn <eglynn@redhat.com>
# Authors: Eoghan Glynn <eglynn@redhat.com>
# Julien Danjou <julien@danjou.info>
#
# 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
@ -21,9 +23,13 @@ from stevedore import extension
from ceilometer.service import prepare_service
from ceilometer.openstack.common import log
from ceilometer.openstack.common import network_utils
from ceilometer.openstack.common import service as os_service
from ceilometer.openstack.common.gettextutils import _
from ceilometer.openstack.common.rpc import service as rpc_service
from ceilometerclient import client as ceiloclient
OPTS = [
cfg.IntOpt('threshold_evaluation_interval',
default=60,
@ -88,3 +94,78 @@ class SingletonAlarmService(os_service.Service):
def singleton_alarm():
prepare_service()
os_service.launch(SingletonAlarmService()).wait()
cfg.CONF.import_opt('host', 'ceilometer.service')
class AlarmNotifierService(rpc_service.Service):
EXTENSIONS_NAMESPACE = "ceilometer.alarm.notifier"
def __init__(self, host, topic):
super(AlarmNotifierService, self).__init__(host, topic, self)
self.notifiers = extension.ExtensionManager(self.EXTENSIONS_NAMESPACE,
invoke_on_load=True)
def start(self):
super(AlarmNotifierService, self).start()
# Add a dummy thread to have wait() working
self.tg.add_timer(604800, lambda: None)
def _handle_action(self, action, alarm, state, reason):
try:
action = network_utils.urlsplit(action)
except Exception:
LOG.error(
_("Unable to parse action %(action)s for alarm %(alarm)s"),
locals())
return
try:
notifier = self.notifiers[action.scheme].obj
except KeyError:
scheme = action.scheme
LOG.error(
_("Action %(scheme)s for alarm %(alarm)s is unknown, "
"cannot notify"),
locals())
return
try:
LOG.debug("Notifying alarm %s with action %s",
alarm, action)
notifier.notify(action, alarm, state, reason)
except Exception:
LOG.exception(_("Unable to notify alarm %s"), alarm)
return
def notify_alarm(self, context, data):
"""Notify that alarm has been triggered.
data should be a dict with the following keys:
- actions, the URL of the action to run;
this is a mapped to extensions automatically
- alarm, the alarm that has been triggered
- state, the new state the alarm transitionned to
- reason, the reason the alarm changed its state
:param context: Request context.
:param data: A dict as described above.
"""
actions = data.get('actions')
if not actions:
LOG.error(_("Unable to notify for an alarm with no action"))
return
for action in actions:
self._handle_action(action,
data.get('alarm'),
data.get('state'),
data.get('reason'))
def alarm_notifier():
prepare_service()
os_service.launch(AlarmNotifierService(
cfg.CONF.host, 'ceilometer.alarm')).wait()

View File

@ -86,6 +86,10 @@ ceilometer.publisher =
ceilometer.alarm =
threshold_eval = ceilometer.alarm.threshold_evaluation:Evaluator
ceilometer.alarm.notifier =
log = ceilometer.alarm.notifier.log:LogAlarmNotifier
test = ceilometer.alarm.notifier.test:TestAlarmNotifier
paste.filter_factory =
swift = ceilometer.objectstore.swift_middleware:filter_factory
@ -97,6 +101,7 @@ console_scripts =
ceilometer-collector = ceilometer.collector.service:collector
ceilometer-collector-udp = ceilometer.collector.service:udp_collector
ceilometer-alarm-singleton = ceilometer.alarm.service:singleton_alarm
ceilometer-alarm-notifier = ceilometer.alarm.notifier:alarm_notifier
[build_sphinx]
all_files = 1

View File

@ -0,0 +1,87 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 eNovance
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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 urlparse
import mock
from ceilometer.alarm import service
from ceilometer.openstack.common import context
from ceilometer.tests import base
class TestAlarmNotifier(base.TestCase):
def setUp(self):
super(TestAlarmNotifier, self).setUp()
self.service = service.AlarmNotifierService('somehost', 'sometopic')
def test_notify_alarm(self):
data = {
'actions': ['test://'],
'alarm': {'name': 'foobar'},
'state': 'ALARM',
'reason': 'Everything is on fire',
}
self.service.notify_alarm(context.get_admin_context(), data)
notifications = self.service.notifiers['test'].obj.notifications
self.assertEqual(len(notifications), 1)
self.assertEqual(notifications[0], (
urlparse.urlsplit(data['actions'][0]),
data['alarm'],
data['state'],
data['reason']))
def test_notify_alarm_no_action(self):
self.service.notify_alarm(context.get_admin_context(), {})
def test_notify_alarm_log_action(self):
self.service.notify_alarm(context.get_admin_context(),
{
'actions': ['log://'],
'alarm': {'name': 'foobar'},
'condition': {'threshold': 42},
})
@staticmethod
def _fake_urlsplit(*args, **kwargs):
raise Exception("Evil urlsplit!")
def test_notify_alarm_invalid_url(self):
with mock.patch('ceilometer.openstack.common.network_utils.urlsplit',
self._fake_urlsplit):
LOG = mock.MagicMock()
with mock.patch('ceilometer.alarm.service.LOG', LOG):
self.service.notify_alarm(
context.get_admin_context(),
{
'actions': ['no-such-action-i-am-sure'],
'alarm': {'name': 'foobar'},
'condition': {'threshold': 42},
})
self.assertTrue(LOG.error.called)
def test_notify_alarm_invalid_action(self):
LOG = mock.MagicMock()
with mock.patch('ceilometer.alarm.service.LOG', LOG):
self.service.notify_alarm(
context.get_admin_context(),
{
'actions': ['no-such-action-i-am-sure://'],
'alarm': {'name': 'foobar'},
'condition': {'threshold': 42},
})
self.assertTrue(LOG.error.called)