Merge "Plug alarm history logic into the API"

This commit is contained in:
Jenkins 2013-09-03 09:19:10 +00:00 committed by Gerrit Code Review
commit 2d3aba2079
4 changed files with 233 additions and 15 deletions

View File

@ -34,10 +34,13 @@
import ast import ast
import datetime import datetime
import inspect import inspect
import json
import uuid import uuid
import pecan import pecan
from pecan import rest from pecan import rest
from oslo.config import cfg
import wsme import wsme
import wsmeext.pecan as wsme_pecan import wsmeext.pecan as wsme_pecan
from wsme import types as wtypes from wsme import types as wtypes
@ -56,6 +59,16 @@ from ceilometer.api import acl
LOG = log.getLogger(__name__) LOG = log.getLogger(__name__)
ALARM_API_OPTS = [
cfg.BoolOpt('record_history',
default=True,
help='Record alarm change events'
),
]
cfg.CONF.register_opts(ALARM_API_OPTS, group='alarm')
operation_kind = wtypes.Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt') operation_kind = wtypes.Enum(str, 'lt', 'le', 'eq', 'ne', 'ge', 'gt')
@ -895,6 +908,9 @@ class AlarmChange(_Base):
"""Representation of an event in an alarm's history """Representation of an event in an alarm's history
""" """
event_id = wtypes.text
"The UUID of the change event"
alarm_id = wtypes.text alarm_id = wtypes.text
"The UUID of the alarm" "The UUID of the alarm"
@ -957,6 +973,28 @@ class AlarmController(rest.RestController):
raise wsme.exc.ClientSideError(error) raise wsme.exc.ClientSideError(error)
return alarms[0] return alarms[0]
def _record_change(self, data, now, on_behalf_of=None, type=None):
if not cfg.CONF.alarm.record_history:
return
type = type or (storage.models.AlarmChange.STATE_TRANSITION
if data.get('state')
else storage.models.AlarmChange.RULE_CHANGE)
detail = json.dumps(utils.stringify_timestamps(data))
user_id = pecan.request.headers.get('X-User-Id')
project_id = pecan.request.headers.get('X-Project-Id')
on_behalf_of = on_behalf_of or project_id
try:
self.conn.record_alarm_change(dict(event_id=str(uuid.uuid4()),
alarm_id=self._id,
type=type,
detail=detail,
user_id=user_id,
project_id=project_id,
on_behalf_of=on_behalf_of,
timestamp=now))
except NotImplementedError:
pass
@wsme_pecan.wsexpose(Alarm, wtypes.text) @wsme_pecan.wsexpose(Alarm, wtypes.text)
def get(self): def get(self):
"""Return this alarm.""" """Return this alarm."""
@ -969,23 +1007,30 @@ class AlarmController(rest.RestController):
# merge the new values from kwargs into the current # merge the new values from kwargs into the current
# alarm "alarm_in". # alarm "alarm_in".
alarm_in = self._alarm() alarm_in = self._alarm()
now = timeutils.utcnow()
change = data.as_dict(storage.models.Alarm)
data.state_timestamp = wsme.Unset data.state_timestamp = wsme.Unset
data.alarm_id = self._id data.alarm_id = self._id
kwargs = data.as_dict(storage.models.Alarm) kwargs = data.as_dict(storage.models.Alarm)
for k, v in kwargs.iteritems(): for k, v in kwargs.iteritems():
setattr(alarm_in, k, v) setattr(alarm_in, k, v)
if k == 'state': if k == 'state':
alarm_in.state_timestamp = timeutils.utcnow() alarm_in.state_timestamp = now
alarm = self.conn.update_alarm(alarm_in) alarm = self.conn.update_alarm(alarm_in)
self._record_change(change, now, on_behalf_of=alarm.project_id)
return Alarm.from_db_model(alarm) return Alarm.from_db_model(alarm)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204) @wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self): def delete(self):
"""Delete this alarm.""" """Delete this alarm."""
# ensure alarm exists before deleting # ensure alarm exists before deleting
alarm_id = self._alarm().alarm_id alarm = self._alarm()
self.conn.delete_alarm(alarm_id) self.conn.delete_alarm(alarm.alarm_id)
change = Alarm.from_db_model(alarm).as_dict(storage.models.Alarm)
self._record_change(change,
timeutils.utcnow(),
type=storage.models.AlarmChange.DELETION)
# TODO(eglynn): add pagination marker to signature once overall # TODO(eglynn): add pagination marker to signature once overall
# API support for pagination is finalized # API support for pagination is finalized
@ -995,10 +1040,13 @@ class AlarmController(rest.RestController):
:param q: Filter rules for the changes to be described. :param q: Filter rules for the changes to be described.
""" """
# ensure per-tenant segregation # allow history to be returned for deleted alarms, but scope changes
self._alarm() # returned to those carried out on behalf of the auth'd tenant, to
# TODO(eglynn): history not yet persisted # avoid inappropriate cross-tenant visibility of alarm history
return [] auth_project = acl.get_limited_to_project(pecan.request.headers)
conn = pecan.request.storage_conn
return [AlarmChange.from_db_model(ac)
for ac in conn.get_alarm_changes(self._id, auth_project)]
class AlarmsController(rest.RestController): class AlarmsController(rest.RestController):
@ -1011,17 +1059,38 @@ class AlarmsController(rest.RestController):
remainder = remainder[:-1] remainder = remainder[:-1]
return AlarmController(alarm_id), remainder return AlarmController(alarm_id), remainder
def _record_creation(self, conn, data, alarm_id, now):
if not cfg.CONF.alarm.record_history:
return
type = storage.models.AlarmChange.CREATION
detail = json.dumps(utils.stringify_timestamps(data))
user_id = pecan.request.headers.get('X-User-Id')
project_id = pecan.request.headers.get('X-Project-Id')
try:
conn.record_alarm_change(dict(event_id=str(uuid.uuid4()),
alarm_id=alarm_id,
type=type,
detail=detail,
user_id=user_id,
project_id=project_id,
on_behalf_of=project_id,
timestamp=now))
except NotImplementedError:
pass
@wsme.validate(Alarm) @wsme.validate(Alarm)
@wsme_pecan.wsexpose(Alarm, body=Alarm, status_code=201) @wsme_pecan.wsexpose(Alarm, body=Alarm, status_code=201)
def post(self, data): def post(self, data):
"""Create a new alarm.""" """Create a new alarm."""
conn = pecan.request.storage_conn conn = pecan.request.storage_conn
now = timeutils.utcnow()
data.alarm_id = str(uuid.uuid4()) data.alarm_id = str(uuid.uuid4())
data.user_id = pecan.request.headers.get('X-User-Id') data.user_id = pecan.request.headers.get('X-User-Id')
data.project_id = pecan.request.headers.get('X-Project-Id') data.project_id = pecan.request.headers.get('X-Project-Id')
data.state_timestamp = wsme.Unset data.state_timestamp = wsme.Unset
data.timestamp = timeutils.utcnow() change = data.as_dict(storage.models.Alarm)
data.timestamp = now
# make sure alarms are unique by name per project. # make sure alarms are unique by name per project.
alarms = list(conn.get_alarms(name=data.name, alarms = list(conn.get_alarms(name=data.name,
@ -1041,6 +1110,7 @@ class AlarmsController(rest.RestController):
raise wsme.exc.ClientSideError(error) raise wsme.exc.ClientSideError(error)
alarm = conn.create_alarm(alarm_in) alarm = conn.create_alarm(alarm_in)
self._record_creation(conn, change, alarm.alarm_id, now)
return Alarm.from_db_model(alarm) return Alarm.from_db_model(alarm)
@wsme_pecan.wsexpose([Alarm], [Query]) @wsme_pecan.wsexpose([Alarm], [Query])

View File

@ -74,3 +74,10 @@ def sanitize_timestamp(timestamp):
if not isinstance(timestamp, datetime.datetime): if not isinstance(timestamp, datetime.datetime):
timestamp = timeutils.parse_isotime(timestamp) timestamp = timeutils.parse_isotime(timestamp)
return timeutils.normalize_time(timestamp) return timeutils.normalize_time(timestamp)
def stringify_timestamps(data):
"""Stringify any datetimes in given dict."""
isa_timestamp = lambda v: isinstance(v, datetime.datetime)
return dict((k, v.isoformat() if isa_timestamp(v) else v)
for (k, v) in data.iteritems())

View File

@ -608,6 +608,14 @@
#threshold_evaluation_interval=60 #threshold_evaluation_interval=60
#
# Options defined in ceilometer.api.controllers.v2
#
# Record alarm change events (boolean value)
#record_history=true
[rpc_notifier2] [rpc_notifier2]
# #

View File

@ -4,6 +4,7 @@
# #
# Author: Mehdi Abaakouk <mehdi.abaakouk@enovance.com> # Author: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
# Angus Salkeld <asalkeld@redhat.com> # Angus Salkeld <asalkeld@redhat.com>
# Eoghan Glynn <eglynn@redhat.com>
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -19,10 +20,14 @@
'''Tests alarm operation '''Tests alarm operation
''' '''
import datetime
import json as jsonutils
import logging import logging
import uuid import uuid
import testscenarios import testscenarios
from oslo.config import cfg
from .base import FunctionalTest from .base import FunctionalTest
from ceilometer.storage.models import Alarm from ceilometer.storage.models import Alarm
@ -47,8 +52,8 @@ class TestAlarms(FunctionalTest,
def setUp(self): def setUp(self):
super(TestAlarms, self).setUp() super(TestAlarms, self).setUp()
self.auth_headers = {'X-User-Id': str(uuid.uuid1()), self.auth_headers = {'X-User-Id': str(uuid.uuid4()),
'X-Project-Id': str(uuid.uuid1())} 'X-Project-Id': str(uuid.uuid4())}
for alarm in [Alarm(name='name1', for alarm in [Alarm(name='name1',
alarm_id='a', alarm_id='a',
counter_name='meter.test', counter_name='meter.test',
@ -178,12 +183,140 @@ class TestAlarms(FunctionalTest,
alarms = list(self.conn.get_alarms()) alarms = list(self.conn.get_alarms())
self.assertEqual(2, len(alarms)) self.assertEqual(2, len(alarms))
def test_get_alarm_history(self): def _get_alarm(self, index):
data = self.get_json('/alarms') data = self.get_json('/alarms')
history = self.get_json('/alarms/%s/history' % data[0]['alarm_id']) return data[index]
def _get_alarm_history(self, alarm, auth_headers=None):
return self.get_json('/alarms/%s/history' % alarm['alarm_id'],
headers=auth_headers or self.auth_headers)
def _update_alarm(self, alarm, data, auth_headers=None):
self.put_json('/alarms/%s' % alarm['alarm_id'],
params=data,
headers=auth_headers or self.auth_headers)
def _assert_is_subset(self, expected, actual):
for k, v in expected.iteritems():
self.assertEqual(v, actual.get(k), 'mismatched field: %s' % k)
self.assertTrue(actual['event_id'] is not None)
def _assert_in_json(self, expected, actual):
for k, v in expected.iteritems():
fragment = jsonutils.dumps({k: v})[1:-1]
self.assertTrue(fragment in actual,
'%s not in %s' % (fragment, actual))
def test_get_unrecorded_alarm_history(self):
cfg.CONF.set_override('record_history', False, group='alarm')
alarm = self._get_alarm(0)
history = self._get_alarm_history(alarm)
self.assertEqual([], history)
self._update_alarm(alarm, dict(name='renamed'))
history = self._get_alarm_history(alarm)
self.assertEqual([], history) self.assertEqual([], history)
def test_get_recorded_alarm_history_on_create(self):
new_alarm = dict(name='new_alarm',
counter_name='other_meter',
comparison_operator='le',
threshold=42.0,
statistic='max')
self.post_json('/alarms', params=new_alarm, status=200,
headers=self.auth_headers)
alarm = self._get_alarm(3)
history = self._get_alarm_history(alarm)
self.assertEqual(1, len(history))
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
on_behalf_of=alarm['project_id'],
project_id=alarm['project_id'],
type='creation',
user_id=alarm['user_id']),
history[0])
self._assert_in_json(new_alarm, history[0]['detail'])
def _do_test_get_recorded_alarm_history_on_update(self,
data,
type,
detail,
auth=None):
alarm = self._get_alarm(0)
history = self._get_alarm_history(alarm)
self.assertEqual([], history)
self._update_alarm(alarm, data, auth)
history = self._get_alarm_history(alarm)
self.assertEqual(1, len(history))
project_id = auth['X-Project-Id'] if auth else alarm['project_id']
user_id = auth['X-User-Id'] if auth else alarm['user_id']
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
detail=detail,
on_behalf_of=alarm['project_id'],
project_id=project_id,
type=type,
user_id=user_id),
history[0])
def test_get_recorded_alarm_history_rule_change(self):
now = datetime.datetime.utcnow().isoformat()
data = dict(name='renamed', timestamp=now)
detail = '{"timestamp": "%s", "name": "renamed"}' % now
self._do_test_get_recorded_alarm_history_on_update(data,
'rule change',
detail)
def test_get_recorded_alarm_history_state_transition(self):
data = dict(state='alarm')
detail = '{"state": "alarm"}'
self._do_test_get_recorded_alarm_history_on_update(data,
'state transition',
detail)
def test_get_recorded_alarm_history_rule_change_on_behalf_of(self):
data = dict(name='renamed')
detail = '{"name": "renamed"}'
auth = {'X-Roles': 'admin',
'X-User-Id': str(uuid.uuid4()),
'X-Project-Id': str(uuid.uuid4())}
self._do_test_get_recorded_alarm_history_on_update(data,
'rule change',
detail,
auth)
def test_get_recorded_alarm_history_segregation(self):
data = dict(name='renamed')
detail = '{"name": "renamed"}'
self._do_test_get_recorded_alarm_history_on_update(data,
'rule change',
detail)
auth = {'X-Roles': 'member',
'X-User-Id': str(uuid.uuid4()),
'X-Project-Id': str(uuid.uuid4())}
history = self._get_alarm_history(self._get_alarm(0), auth)
self.assertEqual([], history)
def test_get_recorded_alarm_history_preserved_after_deletion(self):
alarm = self._get_alarm(0)
history = self._get_alarm_history(alarm)
self.assertEqual([], history)
self._update_alarm(alarm, dict(name='renamed'))
alarm = self._get_alarm(0)
history = self._get_alarm_history(alarm)
self.assertEqual(1, len(history))
self.delete('/alarms/%s' % alarm['alarm_id'],
headers=self.auth_headers,
status=200)
history = self._get_alarm_history(alarm)
self.assertEqual(2, len(history))
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
on_behalf_of=alarm['project_id'],
project_id=alarm['project_id'],
type='deletion',
user_id=alarm['user_id']),
history[0])
self._assert_in_json(alarm, history[0]['detail'])
def test_get_nonexistent_alarm_history(self): def test_get_nonexistent_alarm_history(self):
response = self.get_json('/alarms/%s/history' % 'foobar', # the existence of alarm history is independent of the
expect_errors=True) # continued existence of the alarm itself
self.assertEqual(response.status_int, 400) history = self._get_alarm_history(dict(alarm_id='foobar'))
self.assertEqual([], history)