Merge "Implements complex query functionality for alarm history"
This commit is contained in:
commit
b8dfacf7d8
@ -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.
|
||||
|
@ -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.'))
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user