diff --git a/ceilometer/alarm/service.py b/ceilometer/alarm/service.py index c2687dfb17..04e495b020 100644 --- a/ceilometer/alarm/service.py +++ b/ceilometer/alarm/service.py @@ -232,13 +232,15 @@ class AlarmNotifierService(os_service.Service): EXTENSIONS_NAMESPACE = "ceilometer.alarm.notifier" + notifiers = extension.ExtensionManager(EXTENSIONS_NAMESPACE, + invoke_on_load=True) + notifiers_schemas = notifiers.map(lambda x: x.name) + def __init__(self): super(AlarmNotifierService, self).__init__() transport = messaging.get_transport() self.rpc_server = messaging.get_rpc_server( transport, cfg.CONF.alarm.notifier_rpc_topic, self) - self.notifiers = extension.ExtensionManager(self.EXTENSIONS_NAMESPACE, - invoke_on_load=True) def start(self): super(AlarmNotifierService, self).start() diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index 3f8475fe87..c7e1ac5a20 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -37,6 +37,7 @@ import pytz import uuid from oslo.config import cfg +from oslo.utils import netutils from oslo.utils import strutils from oslo.utils import timeutils import pecan @@ -46,6 +47,7 @@ import wsme from wsme import types as wtypes import wsmeext.pecan as wsme_pecan +from ceilometer.alarm import service as alarm_service from ceilometer.alarm.storage import models as alarm_models from ceilometer.api import acl from ceilometer import messaging @@ -1807,6 +1809,7 @@ class Alarm(_Base): def validate(alarm): Alarm.check_rule(alarm) + Alarm.check_alarm_actions(alarm) if alarm.threshold_rule: # ensure an implicit constraint on project_id is added to # the query if not already present @@ -1845,6 +1848,25 @@ class Alarm(_Base): "cannot be set at the same time") raise ClientSideError(error) + @staticmethod + def check_alarm_actions(alarm): + actions_schema = alarm_service.AlarmNotifierService.notifiers_schemas + for state in state_kind: + actions_name = state.replace(" ", "_") + '_actions' + actions = getattr(alarm, actions_name) + if not actions: + continue + + for action in actions: + try: + url = netutils.urlsplit(action) + except Exception: + error = _("Unable to parse action %s") % action + raise ClientSideError(error) + if url.scheme not in actions_schema: + error = _("Unsupported action %s") % action + raise ClientSideError(error) + @classmethod def sample(cls): return cls(alarm_id=None, diff --git a/ceilometer/tests/api/v2/test_alarm_scenarios.py b/ceilometer/tests/api/v2/test_alarm_scenarios.py index cdb3199006..2481259cd9 100644 --- a/ceilometer/tests/api/v2/test_alarm_scenarios.py +++ b/ceilometer/tests/api/v2/test_alarm_scenarios.py @@ -628,6 +628,64 @@ class TestAlarms(v2.FunctionalTest, 'not valid for this resource', resp.json['error_message']['faultstring']) + def _do_post_alarm_invalid_action(self, ok_actions=[], alarm_actions=[], + insufficient_data_actions=[], + error_message=None): + json = { + 'enabled': False, + 'name': 'added_alarm', + 'state': 'ok', + 'type': 'threshold', + 'ok_actions': ok_actions, + 'alarm_actions': alarm_actions, + 'insufficient_data_actions': insufficient_data_actions, + 'repeat_actions': True, + 'threshold_rule': { + 'meter_name': 'ameter', + 'query': [{'field': 'metadata.field', + 'op': 'eq', + 'value': '5', + 'type': 'string'}], + 'comparison_operator': 'le', + 'statistic': 'count', + 'threshold': 50, + 'evaluation_periods': '3', + 'period': '180', + } + } + resp = self.post_json('/alarms', params=json, status=400, + headers=self.auth_headers) + alarms = list(self.alarm_conn.get_alarms()) + self.assertEqual(4, len(alarms)) + self.assertEqual(error_message, + resp.json['error_message']['faultstring']) + + def test_post_invalid_alarm_ok_actions(self): + self._do_post_alarm_invalid_action( + ok_actions=['spam://something/ok'], + error_message='Unsupported action spam://something/ok') + + def test_post_invalid_alarm_alarm_actions(self): + self._do_post_alarm_invalid_action( + alarm_actions=['spam://something/alarm'], + error_message='Unsupported action spam://something/alarm') + + def test_post_invalid_alarm_insufficient_data_actions(self): + self._do_post_alarm_invalid_action( + insufficient_data_actions=['spam://something/insufficient'], + error_message='Unsupported action spam://something/insufficient') + + @staticmethod + def _fake_urlsplit(*args, **kwargs): + raise Exception("Evil urlsplit!") + + def test_post_invalid_alarm_actions_format(self): + with mock.patch('oslo.utils.netutils.urlsplit', + self._fake_urlsplit): + self._do_post_alarm_invalid_action( + alarm_actions=['http://[::1'], + error_message='Unable to parse action http://[::1') + def test_post_alarm_defaults(self): to_check = { 'enabled': True, @@ -1489,6 +1547,44 @@ class TestAlarms(v2.FunctionalTest, 'Alarm with name=name1 exists', resp.json['error_message']['faultstring']) + def test_put_invalid_alarm_actions(self): + json = { + 'enabled': False, + 'name': 'name1', + 'state': 'ok', + 'type': 'threshold', + 'ok_actions': ['spam://something/ok'], + 'alarm_actions': ['http://something/alarm'], + 'insufficient_data_actions': ['http://something/no'], + 'repeat_actions': True, + 'threshold_rule': { + 'meter_name': 'ameter', + 'query': [{'field': 'metadata.field', + 'op': 'eq', + 'value': '5', + 'type': 'string'}], + 'comparison_operator': 'le', + 'statistic': 'count', + 'threshold': 50, + 'evaluation_periods': 3, + 'period': 180, + } + } + data = self.get_json('/alarms', + q=[{'field': 'name', + 'value': 'name2', + }]) + self.assertEqual(1, len(data)) + alarm_id = data[0]['alarm_id'] + + resp = self.put_json('/alarms/%s' % alarm_id, + expect_errors=True, status=400, + params=json, + headers=self.auth_headers) + self.assertEqual( + 'Unsupported action spam://something/ok', + resp.json['error_message']['faultstring']) + def test_put_alarm_combination_cannot_specify_itself(self): json = { 'name': 'name4',