limit alarm actions

Currently we have alarm quotas, but user still can create a single
alarm with thousands of actions, the worse thing is that normal
user can create log:// and test:// notifiers which they could never
use it, but by doing such thing, normal user can break down ceilometer
services, then maybe the whole server. That is because log:// will
log to local disk (by default) and test:// will continuously consume
server's memory.

This patch adds a configure option to let cloud administrator to
limit each alarm's maximum actions for three states, and disables
normal user to create log:// and test:// to avoid bad things happen.

Change-Id: I7325ab72c94d307075f1317ee0b7b19f30a5d231
Closes-Bug: #1408248
DocImpact
This commit is contained in:
ZhiQiang Fan
2015-03-19 12:16:40 +08:00
parent 2fb046fb66
commit 2d5f4e41ca
2 changed files with 106 additions and 0 deletions

View File

@@ -64,6 +64,10 @@ ALARM_API_OPTS = [
default=None,
help='Maximum number of alarms defined for a project.'
),
cfg.IntOpt('alarm_max_actions',
default=-1,
help='Maximum count of actions for each state of an alarm, '
'non-positive number means no limit.'),
]
cfg.CONF.register_opts(ALARM_API_OPTS, group='alarm')
@@ -306,12 +310,28 @@ class Alarm(base.Base):
@staticmethod
def check_alarm_actions(alarm):
actions_schema = ceilometer_alarm.NOTIFIER_SCHEMAS
max_actions = cfg.CONF.alarm.alarm_max_actions
for state in state_kind:
actions_name = state.replace(" ", "_") + '_actions'
actions = getattr(alarm, actions_name)
if not actions:
continue
action_set = set(actions)
if len(actions) != len(action_set):
LOG.info(_('duplicate actions are found: %s, '
'remove duplicate ones') % actions)
actions = list(action_set)
setattr(alarm, actions_name, actions)
if 0 < max_actions < len(actions):
error = _('%(name)s count exceeds maximum value '
'%(maximum)d') % {"name": actions_name,
"maximum": max_actions}
raise base.ClientSideError(error)
limited = rbac.get_limited_to_project(pecan.request.headers)
for action in actions:
try:
url = netutils.urlsplit(action)
@@ -321,6 +341,10 @@ class Alarm(base.Base):
if url.scheme not in actions_schema:
error = _("Unsupported action %s") % action
raise base.ClientSideError(error)
if limited and url.scheme in ('log', 'test'):
error = _('You are not authorized to create '
'action: %s') % action
raise base.ClientSideError(error, status_code=401)
@classmethod
def sample(cls):

View File

@@ -1709,6 +1709,88 @@ class TestAlarms(v2.FunctionalTest,
self._test_post_alarm_combination_rule_less_than_two_alarms(['a',
'a'])
def test_post_alarm_with_duplicate_actions(self):
body = {
'name': 'dup-alarm-actions',
'type': 'combination',
'combination_rule': {
'alarm_ids': ['a', 'b'],
},
'alarm_actions': ['http://no.where', 'http://no.where']
}
resp = self.post_json('/alarms', params=body,
headers=self.auth_headers)
self.assertEqual(201, resp.status_code)
alarms = list(self.alarm_conn.get_alarms(name='dup-alarm-actions'))
self.assertEqual(1, len(alarms))
self.assertEqual(['http://no.where'], alarms[0].alarm_actions)
def test_post_alarm_with_too_many_actions(self):
self.CONF.set_override('alarm_max_actions', 1, group='alarm')
body = {
'name': 'alarm-with-many-actions',
'type': 'combination',
'combination_rule': {
'alarm_ids': ['a', 'b'],
},
'alarm_actions': ['http://no.where', 'http://no.where2']
}
resp = self.post_json('/alarms', params=body, expect_errors=True,
headers=self.auth_headers)
self.assertEqual(400, resp.status_code)
self.assertEqual("alarm_actions count exceeds maximum value 1",
resp.json['error_message']['faultstring'])
def test_post_alarm_normal_user_set_log_actions(self):
body = {
'name': 'log_alarm_actions',
'type': 'combination',
'combination_rule': {
'alarm_ids': ['a', 'b'],
},
'alarm_actions': ['log://']
}
resp = self.post_json('/alarms', params=body, expect_errors=True,
headers=self.auth_headers)
self.assertEqual(401, resp.status_code)
expected_msg = ("You are not authorized to create action: log://")
self.assertEqual(expected_msg,
resp.json['error_message']['faultstring'])
def test_post_alarm_normal_user_set_test_actions(self):
body = {
'name': 'test_alarm_actions',
'type': 'combination',
'combination_rule': {
'alarm_ids': ['a', 'b'],
},
'alarm_actions': ['test://']
}
resp = self.post_json('/alarms', params=body, expect_errors=True,
headers=self.auth_headers)
self.assertEqual(401, resp.status_code)
expected_msg = ("You are not authorized to create action: test://")
self.assertEqual(expected_msg,
resp.json['error_message']['faultstring'])
def test_post_alarm_admin_user_set_log_test_actions(self):
body = {
'name': 'admin_alarm_actions',
'type': 'combination',
'combination_rule': {
'alarm_ids': ['a', 'b'],
},
'alarm_actions': ['test://', 'log://']
}
headers = self.auth_headers
headers['X-Roles'] = 'admin'
self.post_json('/alarms', params=body, status=201,
headers=headers)
alarms = list(self.alarm_conn.get_alarms(name='admin_alarm_actions'))
self.assertEqual(1, len(alarms))
self.assertEqual(['test://', 'log://'],
alarms[0].alarm_actions)
def test_put_alarm(self):
json = {
'enabled': False,