Merge "Add query support to alarm history API"
This commit is contained in:
commit
291686e322
@ -211,7 +211,7 @@ class Query(_Base):
|
||||
return converted_value
|
||||
|
||||
|
||||
def _sanitize_query(q):
|
||||
def _sanitize_query(q, valid_keys):
|
||||
'''Check the query to see if:
|
||||
1) the request is comming from admin - then allow full visibility
|
||||
2) non-admin - make sure that the query includes the requester's
|
||||
@ -231,7 +231,7 @@ def _sanitize_query(q):
|
||||
i.value)
|
||||
raise wsme.exc.ClientSideError(errstr)
|
||||
|
||||
if not proj_q:
|
||||
if not proj_q and 'on_behalf_of' not in valid_keys:
|
||||
# The user is restricted, but they didn't specify a project
|
||||
# so add it for them.
|
||||
q.append(Query(field='project_id',
|
||||
@ -240,12 +240,19 @@ def _sanitize_query(q):
|
||||
return q
|
||||
|
||||
|
||||
def _query_to_kwargs(query, db_func):
|
||||
def _exclude_from(keys, excluded):
|
||||
if keys and excluded:
|
||||
for key in excluded:
|
||||
if key in keys:
|
||||
keys.remove(key)
|
||||
|
||||
|
||||
def _query_to_kwargs(query, db_func, internal_keys=[]):
|
||||
# TODO(dhellmann): This function needs tests of its own.
|
||||
query = _sanitize_query(query)
|
||||
valid_keys = inspect.getargspec(db_func)[0]
|
||||
if 'self' in valid_keys:
|
||||
valid_keys.remove('self')
|
||||
query = _sanitize_query(query, valid_keys)
|
||||
internal_keys.append('self')
|
||||
_exclude_from(valid_keys, internal_keys)
|
||||
translation = {'user_id': 'user',
|
||||
'project_id': 'project',
|
||||
'resource_id': 'resource'}
|
||||
@ -298,7 +305,9 @@ def _query_to_kwargs(query, db_func):
|
||||
if trans:
|
||||
for k in trans:
|
||||
if k not in valid_keys:
|
||||
raise wsme.exc.UnknownArgument(k, "unrecognized query field")
|
||||
msg = ("unrecognized field in query: %s, valid keys: %s" %
|
||||
(query, valid_keys))
|
||||
raise wsme.exc.UnknownArgument(k, msg)
|
||||
kwargs[k] = trans[k]
|
||||
|
||||
return kwargs
|
||||
@ -1055,8 +1064,10 @@ class AlarmController(rest.RestController):
|
||||
# avoid inappropriate cross-tenant visibility of alarm history
|
||||
auth_project = acl.get_limited_to_project(pecan.request.headers)
|
||||
conn = pecan.request.storage_conn
|
||||
kwargs = _query_to_kwargs(q, conn.get_alarm_changes, ['on_behalf_of'])
|
||||
return [AlarmChange.from_db_model(ac)
|
||||
for ac in conn.get_alarm_changes(self._id, auth_project)]
|
||||
for ac in conn.get_alarm_changes(self._id, auth_project,
|
||||
**kwargs)]
|
||||
|
||||
|
||||
class AlarmsController(rest.RestController):
|
||||
|
@ -231,11 +231,21 @@ class Connection(object):
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
|
@ -598,11 +598,21 @@ class Connection(base.Connection):
|
||||
"""
|
||||
self.db.alarm.remove({'alarm_id': alarm_id})
|
||||
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
raise NotImplementedError('Alarm history not implemented')
|
||||
|
||||
|
@ -598,11 +598,21 @@ class Connection(base.Connection):
|
||||
"""
|
||||
raise NotImplementedError('Alarms not implemented')
|
||||
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
raise NotImplementedError('Alarm history not implemented')
|
||||
|
||||
|
@ -177,11 +177,21 @@ class Connection(base.Connection):
|
||||
"""Delete a alarm
|
||||
"""
|
||||
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
raise NotImplementedError('Alarm history not implemented')
|
||||
|
||||
|
@ -834,15 +834,37 @@ class Connection(base.Connection):
|
||||
"""
|
||||
self.db.alarm.remove({'alarm_id': alarm_id})
|
||||
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
q = dict(alarm_id=alarm_id)
|
||||
if on_behalf_of is not None:
|
||||
q['on_behalf_of'] = on_behalf_of
|
||||
if user is not None:
|
||||
q['user_id'] = user
|
||||
if project is not None:
|
||||
q['project_id'] = project
|
||||
if type is not None:
|
||||
q['type'] = type
|
||||
if start_timestamp or end_timestamp:
|
||||
ts_range = make_timestamp_range(start_timestamp, end_timestamp,
|
||||
start_timestamp_op,
|
||||
end_timestamp_op)
|
||||
if ts_range:
|
||||
q['timestamp'] = ts_range
|
||||
|
||||
sort = [("timestamp", pymongo.DESCENDING)]
|
||||
for alarm_change in self.db.alarm_history.find(q, sort=sort):
|
||||
|
@ -629,11 +629,21 @@ class Connection(base.Connection):
|
||||
session.query(Alarm).filter(Alarm.id == alarm_id).delete()
|
||||
session.flush()
|
||||
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of):
|
||||
def get_alarm_changes(self, alarm_id, on_behalf_of,
|
||||
user=None, project=None, type=None,
|
||||
start_timestamp=None, start_timestamp_op=None,
|
||||
end_timestamp=None, end_timestamp_op=None):
|
||||
"""Yields list of AlarmChanges describing alarm history
|
||||
:param alarm_id: ID of alarm to return changes for
|
||||
:param on_behalf_of: ID of tenant to scope changes query (None for
|
||||
administrative user, indicating all projects)
|
||||
:param user: Optional ID of user to return changes for
|
||||
:param project: Optional ID of project to return changes for
|
||||
:project type: Optional change type
|
||||
:param start_timestamp: Optional modified timestamp start range
|
||||
:param start_timestamp_op: Optional timestamp start range operation
|
||||
:param end_timestamp: Optional modified timestamp end range
|
||||
:param end_timestamp_op: Optional timestamp end range operation
|
||||
"""
|
||||
raise NotImplementedError('Alarm history not implemented')
|
||||
|
||||
|
@ -189,9 +189,17 @@ class TestAlarms(FunctionalTest,
|
||||
self.assertEqual(1, len(match), 'alarm %s not found' % id)
|
||||
return match[0]
|
||||
|
||||
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 _get_alarm_history(self, alarm, auth_headers=None, query=None,
|
||||
expect_errors=False, status=200):
|
||||
url = '/alarms/%s/history' % alarm['alarm_id']
|
||||
if query:
|
||||
url += '?q.op=%(op)s&q.value=%(value)s&q.field=%(field)s' % query
|
||||
resp = self.get_json(url,
|
||||
headers=auth_headers or self.auth_headers,
|
||||
expect_errors=expect_errors)
|
||||
if expect_errors:
|
||||
self.assertEqual(resp.status_code, status)
|
||||
return resp
|
||||
|
||||
def _update_alarm(self, alarm, data, auth_headers=None):
|
||||
self.put_json('/alarms/%s' % alarm['alarm_id'],
|
||||
@ -277,16 +285,58 @@ class TestAlarms(FunctionalTest,
|
||||
'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_state_transition_on_behalf_of(self):
|
||||
# credentials for new non-admin user, on who's behalf the alarm
|
||||
# is created
|
||||
member_user = str(uuid.uuid4())
|
||||
member_project = str(uuid.uuid4())
|
||||
member_auth = {'X-Roles': 'member',
|
||||
'X-User-Id': member_user,
|
||||
'X-Project-Id': member_project}
|
||||
new_alarm = dict(name='new_alarm',
|
||||
meter_name='other_meter',
|
||||
comparison_operator='le',
|
||||
threshold=42.0,
|
||||
statistic='max')
|
||||
self.post_json('/alarms', params=new_alarm, status=200,
|
||||
headers=member_auth)
|
||||
alarm = self.get_json('/alarms', headers=member_auth)[0]
|
||||
|
||||
# effect a state transition as a new administrative user
|
||||
admin_user = str(uuid.uuid4())
|
||||
admin_project = str(uuid.uuid4())
|
||||
admin_auth = {'X-Roles': 'admin',
|
||||
'X-User-Id': admin_user,
|
||||
'X-Project-Id': admin_project}
|
||||
data = dict(state='alarm')
|
||||
self._update_alarm(alarm, data, auth_headers=admin_auth)
|
||||
|
||||
# ensure that both the creation event and state transition
|
||||
# are visible to the non-admin alarm owner and admin user alike
|
||||
for auth in [member_auth, admin_auth]:
|
||||
history = self._get_alarm_history(alarm, auth_headers=auth)
|
||||
self.assertEqual(2, len(history))
|
||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||
detail='{"state": "alarm"}',
|
||||
on_behalf_of=alarm['project_id'],
|
||||
project_id=admin_project,
|
||||
type='state transition',
|
||||
user_id=admin_user),
|
||||
history[0])
|
||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||
on_behalf_of=alarm['project_id'],
|
||||
project_id=member_project,
|
||||
type='creation',
|
||||
user_id=member_user),
|
||||
history[1])
|
||||
self._assert_in_json(new_alarm, history[1]['detail'])
|
||||
|
||||
# ensure on_behalf_of cannot be constrained in an API call
|
||||
query = dict(field='on_behalf_of',
|
||||
op='eq',
|
||||
value=alarm['project_id'])
|
||||
self._get_alarm_history(alarm, auth_headers=auth, query=query,
|
||||
expect_errors=True, status=400)
|
||||
|
||||
def test_get_recorded_alarm_history_segregation(self):
|
||||
data = dict(name='renamed')
|
||||
@ -321,6 +371,39 @@ class TestAlarms(FunctionalTest,
|
||||
history[0])
|
||||
self._assert_in_json(alarm, history[0]['detail'])
|
||||
|
||||
def test_get_constrained_alarm_history(self):
|
||||
alarm = self._get_alarm('a')
|
||||
self._update_alarm(alarm, dict(name='renamed'))
|
||||
now = datetime.datetime.utcnow().isoformat()
|
||||
query = dict(field='timestamp', op='gt', value=now)
|
||||
history = self._get_alarm_history(alarm, query=query)
|
||||
self.assertEqual(0, len(history))
|
||||
query['op'] = 'le'
|
||||
history = self._get_alarm_history(alarm, query=query)
|
||||
self.assertEqual(1, len(history))
|
||||
detail = '{"name": "renamed"}'
|
||||
self._assert_is_subset(dict(alarm_id=alarm['alarm_id'],
|
||||
detail=detail,
|
||||
on_behalf_of=alarm['project_id'],
|
||||
project_id=alarm['project_id'],
|
||||
type='rule change',
|
||||
user_id=alarm['user_id']),
|
||||
history[0])
|
||||
alarm = self._get_alarm('a')
|
||||
self.delete('/alarms/%s' % alarm['alarm_id'],
|
||||
headers=self.auth_headers,
|
||||
status=200)
|
||||
query = dict(field='type', op='eq', value='deletion')
|
||||
history = self._get_alarm_history(alarm, query=query)
|
||||
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='deletion',
|
||||
user_id=alarm['user_id']),
|
||||
history[0])
|
||||
self._assert_in_json(alarm, history[0]['detail'])
|
||||
|
||||
def test_get_nonexistent_alarm_history(self):
|
||||
# the existence of alarm history is independent of the
|
||||
# continued existence of the alarm itself
|
||||
|
Loading…
Reference in New Issue
Block a user