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:
parent
f99d550aa5
commit
928fbe82c8
@ -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])
|
||||
|
@ -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())
|
||||
|
@ -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]
|
||||
|
||||
#
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user