Plug alarm history logic into the API

The alarm scenario tests provide forward-looking coverage
which will be run as the storage backends' support for alarm
history lands.

A new config option alarm.record_history (defaulting to True)
guards the recording of alarm history.

Partially implements bp alarm-audit-api

Change-Id: I0c3bc348c86c7dc645cde3029a6fa76f9f767c37
This commit is contained in:
Eoghan Glynn 2013-08-27 10:18:48 +01:00
parent f99d550aa5
commit 928fbe82c8
4 changed files with 233 additions and 15 deletions

View File

@ -34,10 +34,13 @@
import ast
import datetime
import inspect
import json
import uuid
import pecan
from pecan import rest
from oslo.config import cfg
import wsme
import wsmeext.pecan as wsme_pecan
from wsme import types as wtypes
@ -56,6 +59,16 @@ from ceilometer.api import acl
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')
@ -895,6 +908,9 @@ class AlarmChange(_Base):
"""Representation of an event in an alarm's history
"""
event_id = wtypes.text
"The UUID of the change event"
alarm_id = wtypes.text
"The UUID of the alarm"
@ -957,6 +973,28 @@ class AlarmController(rest.RestController):
raise wsme.exc.ClientSideError(error)
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)
def get(self):
"""Return this alarm."""
@ -969,23 +1007,30 @@ class AlarmController(rest.RestController):
# merge the new values from kwargs into the current
# alarm "alarm_in".
alarm_in = self._alarm()
now = timeutils.utcnow()
change = data.as_dict(storage.models.Alarm)
data.state_timestamp = wsme.Unset
data.alarm_id = self._id
kwargs = data.as_dict(storage.models.Alarm)
for k, v in kwargs.iteritems():
setattr(alarm_in, k, v)
if k == 'state':
alarm_in.state_timestamp = timeutils.utcnow()
alarm_in.state_timestamp = now
alarm = self.conn.update_alarm(alarm_in)
self._record_change(change, now, on_behalf_of=alarm.project_id)
return Alarm.from_db_model(alarm)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self):
"""Delete this alarm."""
# ensure alarm exists before deleting
alarm_id = self._alarm().alarm_id
self.conn.delete_alarm(alarm_id)
alarm = self._alarm()
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
# API support for pagination is finalized
@ -995,10 +1040,13 @@ class AlarmController(rest.RestController):
:param q: Filter rules for the changes to be described.
"""
# ensure per-tenant segregation
self._alarm()
# TODO(eglynn): history not yet persisted
return []
# allow history to be returned for deleted alarms, but scope changes
# returned to those carried out on behalf of the auth'd tenant, to
# avoid inappropriate cross-tenant visibility of alarm history
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):
@ -1011,17 +1059,38 @@ class AlarmsController(rest.RestController):
remainder = remainder[:-1]
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_pecan.wsexpose(Alarm, body=Alarm, status_code=201)
def post(self, data):
"""Create a new alarm."""
conn = pecan.request.storage_conn
now = timeutils.utcnow()
data.alarm_id = str(uuid.uuid4())
data.user_id = pecan.request.headers.get('X-User-Id')
data.project_id = pecan.request.headers.get('X-Project-Id')
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.
alarms = list(conn.get_alarms(name=data.name,
@ -1041,6 +1110,7 @@ class AlarmsController(rest.RestController):
raise wsme.exc.ClientSideError(error)
alarm = conn.create_alarm(alarm_in)
self._record_creation(conn, change, alarm.alarm_id, now)
return Alarm.from_db_model(alarm)
@wsme_pecan.wsexpose([Alarm], [Query])

View File

@ -74,3 +74,10 @@ def sanitize_timestamp(timestamp):
if not isinstance(timestamp, datetime.datetime):
timestamp = timeutils.parse_isotime(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
#
# Options defined in ceilometer.api.controllers.v2
#
# Record alarm change events (boolean value)
#record_history=true
[rpc_notifier2]
#

View File

@ -4,6 +4,7 @@
#
# Author: Mehdi Abaakouk <mehdi.abaakouk@enovance.com>
# Angus Salkeld <asalkeld@redhat.com>
# Eoghan Glynn <eglynn@redhat.com>
#
# 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
@ -19,10 +20,14 @@
'''Tests alarm operation
'''
import datetime
import json as jsonutils
import logging
import uuid
import testscenarios
from oslo.config import cfg
from .base import FunctionalTest
from ceilometer.storage.models import Alarm
@ -47,8 +52,8 @@ class TestAlarms(FunctionalTest,
def setUp(self):
super(TestAlarms, self).setUp()
self.auth_headers = {'X-User-Id': str(uuid.uuid1()),
'X-Project-Id': str(uuid.uuid1())}
self.auth_headers = {'X-User-Id': str(uuid.uuid4()),
'X-Project-Id': str(uuid.uuid4())}
for alarm in [Alarm(name='name1',
alarm_id='a',
counter_name='meter.test',
@ -178,12 +183,140 @@ class TestAlarms(FunctionalTest,
alarms = list(self.conn.get_alarms())
self.assertEqual(2, len(alarms))
def test_get_alarm_history(self):
def _get_alarm(self, index):
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)
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):
response = self.get_json('/alarms/%s/history' % 'foobar',
expect_errors=True)
self.assertEqual(response.status_int, 400)
# the existence of alarm history is independent of the
# continued existence of the alarm itself
history = self._get_alarm_history(dict(alarm_id='foobar'))
self.assertEqual([], history)