Implements complex query functionality for alarm history

New API resource /query/alarms/history has been added.

Implements: blueprint complex-filter-expressions-in-api-queries
Change-Id: I8a98b4edc2dc7afac9261bc0cd83f69d7cadc4b9
This commit is contained in:
Ildiko Vancsa 2013-12-14 12:44:58 +01:00
parent ff83a9d1ea
commit 1e5d8be644
7 changed files with 311 additions and 12 deletions

View File

@ -2052,9 +2052,29 @@ class QuerySamplesController(rest.RestController):
query.limit)]
class QueryAlarmHistoryController(rest.RestController):
"""Provides complex query possibilites for alarm history
"""
@wsme_pecan.wsexpose([AlarmChange], body=ComplexQuery)
def post(self, body):
"""Define query for retrieving AlarmChange data.
:param body: Query rules for the alarm history to be returned.
"""
query = ValidatedComplexQuery(body)
query.validate(visibility_field="on_behalf_of")
conn = pecan.request.storage_conn
return [AlarmChange.from_db_model(s)
for s in conn.query_alarm_history(query.filter_expr,
query.orderby,
query.limit)]
class QueryAlarmsController(rest.RestController):
"""Provides complex query possibilities for alarms
"""
history = QueryAlarmHistoryController()
@wsme_pecan.wsexpose([Alarm], body=ComplexQuery)
def post(self, body):
"""Define query for retrieving Alarm data.

View File

@ -343,3 +343,15 @@ class Connection(object):
raise NotImplementedError(_('Complex query for alarms \
is not implemented.'))
@staticmethod
def query_alarm_history(filter_expr=None, orderby=None, limit=None):
"""Return an iterable of model.AlarmChange objects.
:param filter_expr: Filter expression for query.
:param orderby: List of field name and direction pairs for order by.
:param limit: Maximum number of results to return.
"""
raise NotImplementedError(_('Complex query for alarms \
history is not implemented.'))

View File

@ -791,7 +791,8 @@ class Connection(base.Connection):
filter_expr)
retrieve = {models.Meter: self._retrieve_samples,
models.Alarm: self._retrieve_alarms}
models.Alarm: self._retrieve_alarms,
models.AlarmChange: self._retrieve_alarm_changes}
return retrieve[model](query_filter, orderby_filter, limit)
def query_samples(self, filter_expr=None, orderby=None, limit=None):
@ -1014,6 +1015,21 @@ class Connection(base.Connection):
"""
self.db.alarm.remove({'alarm_id': alarm_id})
def _retrieve_alarm_changes(self, query_filter, orderby, limit):
if limit is not None:
alarms_history = self.db.alarm_history.find(query_filter,
limit=limit,
sort=orderby)
else:
alarms_history = self.db.alarm_history.find(
query_filter, sort=orderby)
for alarm_history in alarms_history:
ah = {}
ah.update(alarm_history)
del ah['_id']
yield models.AlarmChange(**ah)
def get_alarm_changes(self, alarm_id, on_behalf_of,
user=None, project=None, type=None,
start_timestamp=None, start_timestamp_op=None,
@ -1057,12 +1073,10 @@ class Connection(base.Connection):
if ts_range:
q['timestamp'] = ts_range
sort = [("timestamp", pymongo.DESCENDING)]
for alarm_change in self.db.alarm_history.find(q, sort=sort):
ac = {}
ac.update(alarm_change)
del ac['_id']
yield models.AlarmChange(**ac)
return self._retrieve_alarm_changes(q,
[("timestamp",
pymongo.DESCENDING)],
None)
def record_alarm_change(self, alarm_change):
"""Record alarm change event.
@ -1073,3 +1087,11 @@ class Connection(base.Connection):
"""Return an iterable of model.Alarm objects.
"""
return self._retrieve_data(filter_expr, orderby, limit, models.Alarm)
def query_alarm_history(self, filter_expr=None, orderby=None, limit=None):
"""Return an iterable of model.AlarmChange objects.
"""
return self._retrieve_data(filter_expr,
orderby,
limit,
models.AlarmChange)

View File

@ -602,7 +602,8 @@ class Connection(base.Connection):
table)
retrieve = {models.Meter: self._retrieve_samples,
models.Alarm: self._retrieve_alarms}
models.Alarm: self._retrieve_alarms,
models.AlarmChange: self._retrieve_alarm_history}
return retrieve[table](query)
def query_samples(self, filter_expr=None, orderby=None, limit=None):
@ -840,6 +841,17 @@ class Connection(base.Connection):
"""
return self._retrieve_data(filter_expr, orderby, limit, models.Alarm)
def _retrieve_alarm_history(self, query):
return (self._row_to_alarm_change_model(x) for x in query.all())
def query_alarm_history(self, filter_expr=None, orderby=None, limit=None):
"""Return an iterable of model.AlarmChange objects.
"""
return self._retrieve_data(filter_expr,
orderby,
limit,
models.AlarmChange)
def get_alarm_changes(self, alarm_id, on_behalf_of,
user=None, project=None, type=None,
start_timestamp=None, start_timestamp_op=None,
@ -896,7 +908,7 @@ class Connection(base.Connection):
models.AlarmChange.timestamp < end_timestamp)
query = query.order_by(desc(models.AlarmChange.timestamp))
return (self._row_to_alarm_change_model(x) for x in query.all())
return self._retrieve_alarm_history(query)
def record_alarm_change(self, alarm_change):
"""Record alarm change event.

View File

@ -340,3 +340,108 @@ class TestQueryAlarmsController(tests_api.FunctionalTest,
self.assertEqual(400, data.status_int)
self.assertIn("Limit should be positive", data.body)
class TestQueryAlarmsHistoryController(
tests_api.FunctionalTest, tests_db.MixinTestsWithBackendScenarios):
def setUp(self):
super(TestQueryAlarmsHistoryController, self).setUp()
self.url = '/query/alarms/history'
for id in [1, 2]:
for type in ["creation", "state transition"]:
for date in [datetime.datetime(2013, 1, 1),
datetime.datetime(2013, 2, 2)]:
event_id = "-".join([str(id), type, date.isoformat()])
alarm_change = {"event_id": event_id,
"alarm_id": "alarm-id%d" % id,
"type": type,
"detail": "",
"user_id": "user-id%d" % id,
"project_id": "project-id%d" % id,
"on_behalf_of": "project-id%d" % id,
"timestamp": date}
self.conn.record_alarm_change(alarm_change)
def test_query_all(self):
data = self.post_json(self.url,
params={})
self.assertEqual(8, len(data.json))
def test_filter_with_isotime(self):
date_time = datetime.datetime(2013, 1, 1)
isotime = date_time.isoformat()
data = self.post_json(self.url,
params={"filter":
'{">": {"timestamp":"'
+ isotime + '"}}'})
self.assertEqual(4, len(data.json))
for history in data.json:
result_time = timeutils.parse_isotime(history['timestamp'])
result_time = result_time.replace(tzinfo=None)
self.assertTrue(result_time > date_time)
def test_non_admin_tenant_sees_only_its_own_project(self):
data = self.post_json(self.url,
params={},
headers=non_admin_header)
for history in data.json:
self.assertEqual("project-id1", history['on_behalf_of'])
def test_non_admin_tenant_cannot_query_others_project(self):
data = self.post_json(self.url,
params={"filter":
'{"=": {"on_behalf_of":'
+ ' "project-id2"}}'},
expect_errors=True,
headers=non_admin_header)
self.assertEqual(401, data.status_int)
self.assertIn("Not Authorized to access project project-id2",
data.body)
def test_non_admin_tenant_can_explicitly_filter_for_own_project(self):
data = self.post_json(self.url,
params={"filter":
'{"=": {"on_behalf_of":'
+ ' "project-id1"}}'},
headers=non_admin_header)
for history in data.json:
self.assertEqual("project-id1", history['on_behalf_of'])
def test_admin_tenant_sees_every_project(self):
data = self.post_json(self.url,
params={},
headers=admin_header)
self.assertEqual(8, len(data.json))
for history in data.json:
self.assertIn(history['on_behalf_of'],
(["project-id1", "project-id2"]))
def test_query_with_filter_orderby_and_limit(self):
data = self.post_json(self.url,
params={"filter": '{"=": {"type": "creation"}}',
"orderby": '[{"timestamp": "DESC"}]',
"limit": 3})
self.assertEqual(3, len(data.json))
self.assertEqual(["2013-02-02T00:00:00",
"2013-02-02T00:00:00",
"2013-01-01T00:00:00"],
[h["timestamp"] for h in data.json])
for history in data.json:
self.assertEqual("creation", history["type"])
def test_limit_should_be_positive(self):
data = self.post_json(self.url,
params={"limit": 0},
expect_errors=True)
self.assertEqual(400, data.status_int)
self.assertIn("Limit should be positive", data.body)

View File

@ -2331,6 +2331,131 @@ class ComplexAlarmQueryTest(AlarmTestBase,
self.assertTrue(a.enabled)
class ComplexAlarmHistoryQueryTest(AlarmTestBase,
tests_db.MixinTestsWithBackendScenarios):
def setUp(self):
super(DBTestBase, self).setUp()
self.filter_expr = {"and":
[{"or":
[{"=": {"type": "rule change"}},
{"=": {"type": "state transition"}}]},
{"=": {"alarm_id": "0r4ng3"}}]}
self.add_some_alarms()
self.prepare_alarm_history()
def prepare_alarm_history(self):
alarms = list(self.conn.get_alarms())
for alarm in alarms:
i = alarms.index(alarm)
alarm_change = dict(event_id=
"16fd2706-8baf-433b-82eb-8c7fada847c%s" % i,
alarm_id=alarm.alarm_id,
type=models.AlarmChange.CREATION,
detail="detail %s" % alarm.name,
user_id=alarm.user_id,
project_id=alarm.project_id,
on_behalf_of=alarm.project_id,
timestamp=datetime.datetime(2012, 9, 24,
7 + i,
30 + i))
self.conn.record_alarm_change(alarm_change=alarm_change)
alarm_change2 = dict(event_id=
"16fd2706-8baf-433b-82eb-8c7fada847d%s" % i,
alarm_id=alarm.alarm_id,
type=models.AlarmChange.RULE_CHANGE,
detail="detail %s" % i,
user_id=alarm.user_id,
project_id=alarm.project_id,
on_behalf_of=alarm.project_id,
timestamp=datetime.datetime(2012, 9, 25,
10 + i,
30 + i))
self.conn.record_alarm_change(alarm_change=alarm_change2)
alarm_change3 = dict(event_id=
"16fd2706-8baf-433b-82eb-8c7fada847e%s"
% i,
alarm_id=alarm.alarm_id,
type=models.AlarmChange.STATE_TRANSITION,
detail="detail %s" % (i + 1),
user_id=alarm.user_id,
project_id=alarm.project_id,
on_behalf_of=alarm.project_id,
timestamp=datetime.datetime(2012, 9, 26,
10 + i,
30 + i))
if alarm.name == "red-alert":
alarm_change3['on_behalf_of'] = 'and-da-girls'
self.conn.record_alarm_change(alarm_change=alarm_change3)
if alarm.name in ["red-alert", "yellow-alert"]:
alarm_change4 = dict(event_id=
"16fd2706-8baf-433b-82eb-8c7fada847f%s"
% i,
alarm_id=alarm.alarm_id,
type=models.AlarmChange.DELETION,
detail="detail %s" % (i + 2),
user_id=alarm.user_id,
project_id=alarm.project_id,
on_behalf_of=alarm.project_id,
timestamp=datetime.datetime(2012, 9, 27,
10 + i,
30 + i))
self.conn.record_alarm_change(alarm_change=alarm_change4)
def test_alarm_history_with_no_filter(self):
history = list(self.conn.query_alarm_history())
self.assertEqual(11, len(history))
def test_alarm_history_with_no_filter_and_limit(self):
history = list(self.conn.query_alarm_history(limit=3))
self.assertEqual(3, len(history))
def test_alarm_history_with_filter(self):
history = list(
self.conn.query_alarm_history(filter_expr=self.filter_expr))
self.assertEqual(2, len(history))
def test_alarm_history_with_filter_and_orderby(self):
history = list(
self.conn.query_alarm_history(filter_expr=self.filter_expr,
orderby=[{"timestamp":
"asc"}]))
self.assertEqual([models.AlarmChange.RULE_CHANGE,
models.AlarmChange.STATE_TRANSITION],
[h.type for h in history])
def test_alarm_history_with_filter_and_orderby_and_limit(self):
history = list(
self.conn.query_alarm_history(filter_expr=self.filter_expr,
orderby=[{"timestamp":
"asc"}],
limit=1))
self.assertEqual(models.AlarmChange.RULE_CHANGE, history[0].type)
def test_alarm_history_with_on_behalf_of_filter(self):
filter_expr = {"=": {"on_behalf_of": "and-da-girls"}}
history = list(self.conn.query_alarm_history(filter_expr=filter_expr))
self.assertEqual(1, len(history))
self.assertEqual("16fd2706-8baf-433b-82eb-8c7fada847e0",
history[0].event_id)
def test_alarm_history_with_alarm_id_as_filter(self):
filter_expr = {"=": {"alarm_id": "r3d"}}
history = list(self.conn.query_alarm_history(filter_expr=filter_expr,
orderby=[{"timestamp":
"asc"}]))
self.assertEqual(4, len(history))
self.assertEqual([models.AlarmChange.CREATION,
models.AlarmChange.RULE_CHANGE,
models.AlarmChange.STATE_TRANSITION,
models.AlarmChange.DELETION],
[h.type for h in history])
class EventTestBase(tests_db.TestBase,
tests_db.MixinTestsWithBackendScenarios):
"""Separate test base class because we don't want to

View File

@ -94,9 +94,9 @@ field of *Sample*).
Complex Query
+++++++++++++
The filter expressions of the Complex Query feature operate on the fields
of *Sample* and *Alarm*. The following comparison operators are supported: *=*,
*!=*, *<*, *<=*, *>* and *>=*; and the following logical operators can be
used: *and* and *or*.
of *Sample*, *Alarm* and *AlarmChange*. The following comparison operators are
supported: *=*, *!=*, *<*, *<=*, *>* and *>=*; and the following logical operators
can be used: *and* and *or*.
Complex Query supports defining the list of orderby expressions in the form
of [{"field_name": "asc"}, {"field_name2": "desc"}, ...].
@ -111,6 +111,9 @@ The *filter*, *orderby* and *limit* are all optional fields in a query.
.. rest-controller:: ceilometer.api.controllers.v2:QueryAlarmsController
:webprefix: /v2/query/alarms
.. rest-controller:: ceilometer.api.controllers.v2:QueryAlarmHistoryController
:webprefix: /v2/query/alarms/history
.. autotype:: ceilometer.api.controllers.v2.ComplexQuery
:members: