Merge "Add the ability to filter on metadata."

This commit is contained in:
Jenkins 2012-12-07 09:03:21 +00:00 committed by Gerrit Code Review
commit cc2ab04937
10 changed files with 172 additions and 36 deletions

View File

@ -100,14 +100,21 @@ def request_wants_html():
flask.request.accept_mimetypes['application/json'] flask.request.accept_mimetypes['application/json']
def _get_metaquery(args):
return dict((k, v)
for (k, v) in args.iteritems()
if k.startswith('metadata.'))
## APIs for working with meters. ## APIs for working with meters.
@blueprint.route('/meters') @blueprint.route('/meters')
def list_meters_all(): def list_meters_all():
"""Return a list of meters. """Return a list of meters.
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
meters = flask.request.storage_conn.get_meters() rq = flask.request
meters = rq.storage_conn.get_meters(metaquery=_get_metaquery(rq.args))
return flask.jsonify(meters=list(meters)) return flask.jsonify(meters=list(meters))
@ -116,8 +123,11 @@ def list_meters_by_resource(resource):
"""Return a list of meters by resource. """Return a list of meters by resource.
:param resource: The ID of the resource. :param resource: The ID of the resource.
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
meters = flask.request.storage_conn.get_meters(resource=resource) rq = flask.request
meters = rq.storage_conn.get_meters(resource=resource,
metaquery=_get_metaquery(rq.args))
return flask.jsonify(meters=list(meters)) return flask.jsonify(meters=list(meters))
@ -126,8 +136,11 @@ def list_meters_by_user(user):
"""Return a list of meters by user. """Return a list of meters by user.
:param user: The ID of the owning user. :param user: The ID of the owning user.
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
meters = flask.request.storage_conn.get_meters(user=user) rq = flask.request
meters = rq.storage_conn.get_meters(user=user,
metaquery=_get_metaquery(rq.args))
return flask.jsonify(meters=list(meters)) return flask.jsonify(meters=list(meters))
@ -136,8 +149,11 @@ def list_meters_by_project(project):
"""Return a list of meters by project. """Return a list of meters by project.
:param project: The ID of the owning project. :param project: The ID of the owning project.
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
meters = flask.request.storage_conn.get_meters(project=project) rq = flask.request
meters = rq.storage_conn.get_meters(project=project,
metaquery=_get_metaquery(rq.args))
return flask.jsonify(meters=list(meters)) return flask.jsonify(meters=list(meters))
@ -146,8 +162,11 @@ def list_meters_by_source(source):
"""Return a list of meters by source. """Return a list of meters by source.
:param source: The ID of the owning source. :param source: The ID of the owning source.
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
meters = flask.request.storage_conn.get_meters(source=source) rq = flask.request
meters = rq.storage_conn.get_meters(source=source,
metaquery=_get_metaquery(rq.args))
return flask.jsonify(meters=list(meters)) return flask.jsonify(meters=list(meters))
@ -156,13 +175,15 @@ def list_meters_by_source(source):
def _list_resources(source=None, user=None, project=None): def _list_resources(source=None, user=None, project=None):
"""Return a list of resource identifiers. """Return a list of resource identifiers.
""" """
q_ts = _get_query_timestamps(flask.request.args) rq = flask.request
resources = flask.request.storage_conn.get_resources( q_ts = _get_query_timestamps(rq.args)
resources = rq.storage_conn.get_resources(
source=source, source=source,
user=user, user=user,
project=project, project=project,
start_timestamp=q_ts['start_timestamp'], start_timestamp=q_ts['start_timestamp'],
end_timestamp=q_ts['end_timestamp'], end_timestamp=q_ts['end_timestamp'],
metaquery=_get_metaquery(rq.args),
) )
return flask.jsonify(resources=list(resources)) return flask.jsonify(resources=list(resources))
@ -178,6 +199,7 @@ def list_resources_by_project(project):
:param end_timestamp: Limits resources by last update time < this value. :param end_timestamp: Limits resources by last update time < this value.
(optional) (optional)
:type end_timestamp: ISO date in UTC :type end_timestamp: ISO date in UTC
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
return _list_resources(project=project) return _list_resources(project=project)
@ -192,6 +214,7 @@ def list_all_resources():
:param end_timestamp: Limits resources by last update time < this value. :param end_timestamp: Limits resources by last update time < this value.
(optional) (optional)
:type end_timestamp: ISO date in UTC :type end_timestamp: ISO date in UTC
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
return _list_resources() return _list_resources()
@ -217,6 +240,7 @@ def list_resources_by_source(source):
:param end_timestamp: Limits resources by last update time < this value. :param end_timestamp: Limits resources by last update time < this value.
(optional) (optional)
:type end_timestamp: ISO date in UTC :type end_timestamp: ISO date in UTC
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
return _list_resources(source=source) return _list_resources(source=source)
@ -232,6 +256,7 @@ def list_resources_by_user(user):
:param end_timestamp: Limits resources by last update time < this value. :param end_timestamp: Limits resources by last update time < this value.
(optional) (optional)
:type end_timestamp: ISO date in UTC :type end_timestamp: ISO date in UTC
:param metadata.<key> match on the metadata within the resource. (optional)
""" """
return _list_resources(user=user) return _list_resources(user=user)
@ -308,6 +333,7 @@ def _list_events(meter,
resource=resource, resource=resource,
start=q_ts['start_timestamp'], start=q_ts['start_timestamp'],
end=q_ts['end_timestamp'], end=q_ts['end_timestamp'],
metaquery=_get_metaquery(flask.request.args),
) )
events = list(flask.request.storage_conn.get_raw_events(f)) events = list(flask.request.storage_conn.get_raw_events(f))
jsonified = flask.jsonify(events=events) jsonified = flask.jsonify(events=events)

View File

@ -80,9 +80,10 @@ class EventFilter(object):
:param resource: Optional filter for resource id. :param resource: Optional filter for resource id.
:param meter: Optional filter for meter type using the meter name. :param meter: Optional filter for meter type using the meter name.
:param source: Optional source filter. :param source: Optional source filter.
:param metaquery: Optional filter on the metadata
""" """
def __init__(self, user=None, project=None, start=None, end=None, def __init__(self, user=None, project=None, start=None, end=None,
resource=None, meter=None, source=None): resource=None, meter=None, source=None, metaquery={}):
self.user = user self.user = user
self.project = project self.project = project
self.start = self._sanitize_timestamp(start) self.start = self._sanitize_timestamp(start)
@ -90,6 +91,7 @@ class EventFilter(object):
self.resource = resource self.resource = resource
self.meter = meter self.meter = meter
self.source = source self.source = source
self.metaquery = metaquery
def _sanitize_timestamp(self, timestamp): def _sanitize_timestamp(self, timestamp):
"""Return a naive utc datetime object""" """Return a naive utc datetime object"""

View File

@ -78,7 +78,8 @@ class Connection(object):
@abc.abstractmethod @abc.abstractmethod
def get_resources(self, user=None, project=None, source=None, def get_resources(self, user=None, project=None, source=None,
start_timestamp=None, end_timestamp=None): start_timestamp=None, end_timestamp=None,
metaquery={}):
"""Return an iterable of dictionaries containing resource information. """Return an iterable of dictionaries containing resource information.
{ 'resource_id': UUID of the resource, { 'resource_id': UUID of the resource,
@ -94,6 +95,26 @@ class Connection(object):
:param source: Optional source filter. :param source: Optional source filter.
:param start_timestamp: Optional modified timestamp start range. :param start_timestamp: Optional modified timestamp start range.
:param end_timestamp: Optional modified timestamp end range. :param end_timestamp: Optional modified timestamp end range.
:param metaquery: Optional dict with metadata to match on..
"""
@abc.abstractmethod
def get_meters(self, user=None, project=None, resource=None, source=None,
metaquery={}):
"""Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter,
'type': type of the meter (guage, counter),
'resource_id': UUID of the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
}
:param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource.
:param resource: Optional resource filter.
:param source: Optional source filter.
:param metaquery: Optional dict with metadata to match on.
""" """
@abc.abstractmethod @abc.abstractmethod
@ -133,14 +154,3 @@ class Connection(object):
( datetime.datetime(), datetime.datetime() ) ( datetime.datetime(), datetime.datetime() )
""" """
@abc.abstractmethod
def get_meters(self, user=None, project=None, resource=None, source=None):
"""Return a list of meters.
{'resource_id': UUID string for the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
'name': The name of the meter,
'type': The meter type (gauge, counter, diff),
}
"""

View File

@ -69,18 +69,28 @@ class Connection(base.Connection):
""" """
def get_resources(self, user=None, project=None, source=None, def get_resources(self, user=None, project=None, source=None,
start_timestamp=None, end_timestamp=None): start_timestamp=None, end_timestamp=None,
"""Return an iterable of tuples containing resource ids and metaquery={}):
the most recent version of the metadata for the resource. """Return an iterable of dictionaries containing resource information.
{ 'resource_id': UUID of the resource,
'project_id': UUID of project owning the resource,
'user_id': UUID of user owning the resource,
'timestamp': UTC datetime of last update to the resource,
'metadata': most current metadata for the resource,
'meter': list of the meters reporting data for the resource,
}
:param user: Optional ID for user that owns the resource. :param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource. :param project: Optional ID for project that owns the resource.
:param source: Optional source filter. :param source: Optional source filter.
:param start_timestamp: Optional modified timestamp start range. :param start_timestamp: Optional modified timestamp start range.
:param end_timestamp: Optional modified timestamp end range. :param end_timestamp: Optional modified timestamp end range.
:param metaquery: Optional dict with metadata to match on.
""" """
def get_meters(self, user=None, project=None, resource=None, source=None): def get_meters(self, user=None, project=None, resource=None, source=None,
metaquery={}):
"""Return an iterable of dictionaries containing meter information. """Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter, { 'name': name of the meter,
@ -92,8 +102,9 @@ class Connection(base.Connection):
:param user: Optional ID for user that owns the resource. :param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource. :param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource. :param resource: Optional resource filter.
:param source: Optional source filter. :param source: Optional source filter.
:param metaquery: Optional dict with metadata to match on.
""" """
def get_raw_events(self, event_filter): def get_raw_events(self, event_filter):

View File

@ -114,6 +114,10 @@ def make_query_from_filter(event_filter, require_meter=True):
if event_filter.source: if event_filter.source:
q['source'] = event_filter.source q['source'] = event_filter.source
# so the events call metadata resource_metadata, so we convert
# to that.
q.update(dict(('resource_%s' % k, v)
for (k, v) in event_filter.metaquery.iteritems()))
return q return q
@ -299,7 +303,8 @@ class Connection(base.Connection):
return sorted(self.db.project.find(q).distinct('_id')) return sorted(self.db.project.find(q).distinct('_id'))
def get_resources(self, user=None, project=None, source=None, def get_resources(self, user=None, project=None, source=None,
start_timestamp=None, end_timestamp=None): start_timestamp=None, end_timestamp=None,
metaquery={}):
"""Return an iterable of dictionaries containing resource information. """Return an iterable of dictionaries containing resource information.
{ 'resource_id': UUID of the resource, { 'resource_id': UUID of the resource,
@ -315,6 +320,7 @@ class Connection(base.Connection):
:param source: Optional source filter. :param source: Optional source filter.
:param start_timestamp: Optional modified timestamp start range. :param start_timestamp: Optional modified timestamp start range.
:param end_timestamp: Optional modified timestamp end range. :param end_timestamp: Optional modified timestamp end range.
:param metaquery: Optional dict with metadata to match on.
""" """
q = {} q = {}
if user is not None: if user is not None:
@ -323,6 +329,8 @@ class Connection(base.Connection):
q['project_id'] = project q['project_id'] = project
if source is not None: if source is not None:
q['source'] = source q['source'] = source
q.update(metaquery)
# FIXME(dhellmann): This may not perform very well, # FIXME(dhellmann): This may not perform very well,
# but doing any better will require changing the database # but doing any better will require changing the database
# schema and that will need more thought than I have time # schema and that will need more thought than I have time
@ -347,7 +355,8 @@ class Connection(base.Connection):
del r['_id'] del r['_id']
yield r yield r
def get_meters(self, user=None, project=None, resource=None, source=None): def get_meters(self, user=None, project=None, resource=None, source=None,
metaquery={}):
"""Return an iterable of dictionaries containing meter information. """Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter, { 'name': name of the meter,
@ -359,8 +368,9 @@ class Connection(base.Connection):
:param user: Optional ID for user that owns the resource. :param user: Optional ID for user that owns the resource.
:param project: Optional ID for project that owns the resource. :param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource. :param resource: Optional resource filter.
:param source: Optional source filter. :param source: Optional source filter.
:param metaquery: Optional dict with metadata to match on.
""" """
q = {} q = {}
if user is not None: if user is not None:
@ -371,6 +381,8 @@ class Connection(base.Connection):
q['_id'] = resource q['_id'] = resource
if source is not None: if source is not None:
q['source'] = source q['source'] = source
q.update(metaquery)
for r in self.db.resource.find(q): for r in self.db.resource.find(q):
for r_meter in r['meter']: for r_meter in r['meter']:
m = {} m = {}

View File

@ -113,6 +113,9 @@ def make_query_from_filter(query, event_filter, require_meter=True):
if event_filter.resource: if event_filter.resource:
query = query.filter_by(resource_id=event_filter.resource) query = query.filter_by(resource_id=event_filter.resource)
if event_filter.metaquery is not None and len(event_filter.metaquery) > 0:
raise NotImplementedError('metaquery not implemented')
return query return query
@ -212,7 +215,8 @@ class Connection(base.Connection):
return (x[0] for x in query.all()) return (x[0] for x in query.all())
def get_resources(self, user=None, project=None, source=None, def get_resources(self, user=None, project=None, source=None,
start_timestamp=None, end_timestamp=None): start_timestamp=None, end_timestamp=None,
metaquery=None):
"""Return an iterable of dictionaries containing resource information. """Return an iterable of dictionaries containing resource information.
{ 'resource_id': UUID of the resource, { 'resource_id': UUID of the resource,
@ -228,6 +232,7 @@ class Connection(base.Connection):
:param source: Optional source filter. :param source: Optional source filter.
:param start_timestamp: Optional modified timestamp start range. :param start_timestamp: Optional modified timestamp start range.
:param end_timestamp: Optional modified timestamp end range. :param end_timestamp: Optional modified timestamp end range.
:param metaquery: Optional dict with metadata to match on.
""" """
query = model_query(Resource, session=self.session) query = model_query(Resource, session=self.session)
if user is not None: if user is not None:
@ -242,6 +247,8 @@ class Connection(base.Connection):
query = query.filter(Resource.project_id == project) query = query.filter(Resource.project_id == project)
query = query.options( query = query.options(
sqlalchemy_session.sqlalchemy.orm.joinedload('meters')) sqlalchemy_session.sqlalchemy.orm.joinedload('meters'))
if metaquery is not None:
raise NotImplementedError('metaquery not implemented')
for resource in query.all(): for resource in query.all():
r = row2dict(resource) r = row2dict(resource)
@ -257,8 +264,8 @@ class Connection(base.Connection):
del r['meters'] del r['meters']
yield r yield r
def get_meters(self, user=None, project=None, source=None, def get_meters(self, user=None, project=None, resource=None, source=None,
resource=None): metaquery={}):
"""Return an iterable of dictionaries containing meter information. """Return an iterable of dictionaries containing meter information.
{ 'name': name of the meter, { 'name': name of the meter,
@ -272,6 +279,7 @@ class Connection(base.Connection):
:param project: Optional ID for project that owns the resource. :param project: Optional ID for project that owns the resource.
:param resource: Optional ID of the resource. :param resource: Optional ID of the resource.
:param source: Optional source filter. :param source: Optional source filter.
:param metaquery: Optional dict with metadata to match on.
""" """
query = model_query(Resource, session=self.session) query = model_query(Resource, session=self.session)
if user is not None: if user is not None:
@ -284,6 +292,8 @@ class Connection(base.Connection):
query = query.filter(Resource.project_id == project) query = query.filter(Resource.project_id == project)
query = query.options( query = query.options(
sqlalchemy_session.sqlalchemy.orm.joinedload('meters')) sqlalchemy_session.sqlalchemy.orm.joinedload('meters'))
if len(metaquery) > 0:
raise NotImplementedError('metaquery not implemented')
for resource in query.all(): for resource in query.all():
meter_names = set() meter_names = set()

View File

@ -124,3 +124,18 @@ class TestListEvents(tests_api.TestBase):
start_timestamp=datetime.datetime(2012, 7, 2, 10, 41), start_timestamp=datetime.datetime(2012, 7, 2, 10, 41),
end_timestamp=datetime.datetime(2012, 7, 2, 10, 42)) end_timestamp=datetime.datetime(2012, 7, 2, 10, 42))
self.assertEquals(1, len(data['events'])) self.assertEquals(1, len(data['events']))
def test_metaquery1(self):
q = '/sources/source1/meters/instance'
data = self.get('%s?metadata.tag=self.counter2' % q)
self.assertEquals(1, len(data['events']))
def test_metaquery2(self):
q = '/sources/source1/meters/instance'
data = self.get('%s?metadata.tag=self.counter' % q)
self.assertEquals(2, len(data['events']))
def test_metaquery3(self):
q = '/sources/source1/meters/instance'
data = self.get('%s?metadata.display_name=test-server' % q)
self.assertEquals(3, len(data['events']))

View File

@ -74,7 +74,7 @@ class TestListMeters(tests_api.TestBase):
'resource-id2', 'resource-id2',
timestamp=datetime.datetime(2012, 7, 2, 10, 41), timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server', resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2', 'tag': 'two.counter',
}), }),
counter.Counter( counter.Counter(
'meter.test', 'meter.test',
@ -85,7 +85,7 @@ class TestListMeters(tests_api.TestBase):
'resource-id3', 'resource-id3',
timestamp=datetime.datetime(2012, 7, 2, 10, 42), timestamp=datetime.datetime(2012, 7, 2, 10, 42),
resource_metadata={'display_name': 'test-server', resource_metadata={'display_name': 'test-server',
'tag': 'self.counter3', 'tag': 'three.counter',
}), }),
counter.Counter( counter.Counter(
'meter.mine', 'meter.mine',
@ -96,7 +96,7 @@ class TestListMeters(tests_api.TestBase):
'resource-id4', 'resource-id4',
timestamp=datetime.datetime(2012, 7, 2, 10, 43), timestamp=datetime.datetime(2012, 7, 2, 10, 43),
resource_metadata={'display_name': 'test-server', resource_metadata={'display_name': 'test-server',
'tag': 'self.counter4', 'tag': 'four.counter',
})]: })]:
msg = meter.meter_message_from_counter(cnt, msg = meter.meter_message_from_counter(cnt,
cfg.CONF.metering_secret, cfg.CONF.metering_secret,
@ -153,3 +153,11 @@ class TestListMeters(tests_api.TestBase):
def test_with_project_non_existent(self): def test_with_project_non_existent(self):
data = self.get('/projects/jd-was-here/meters') data = self.get('/projects/jd-was-here/meters')
self.assertEquals(data['meters'], []) self.assertEquals(data['meters'], [])
def test_metaquery1(self):
data = self.get('/meters?metadata.tag=self.counter')
self.assertEquals(1, len(data['meters']))
def test_metaquery2(self):
data = self.get('/meters?metadata.tag=four.counter')
self.assertEquals(1, len(data['meters']))

View File

@ -166,3 +166,13 @@ class TestListResources(tests_api.TestBase):
def test_with_project_non_existent(self): def test_with_project_non_existent(self):
data = self.get('/projects/jd-was-here/resources') data = self.get('/projects/jd-was-here/resources')
self.assertEquals(data['resources'], []) self.assertEquals(data['resources'], [])
def test_metaquery1(self):
q = '/sources/test_list_resources/resources'
data = self.get('%s?metadata.display_name=test-server' % q)
self.assertEquals(3, len(data['resources']))
def test_metaquery2(self):
q = '/sources/test_list_resources/resources'
data = self.get('%s?metadata.tag=self.counter4' % q)
self.assertEquals(1, len(data['resources']))

View File

@ -299,6 +299,21 @@ class ResourceTest(SQLAlchemyEngineTestBase):
ids = set(r['resource_id'] for r in resources) ids = set(r['resource_id'] for r in resources)
assert ids == set(['resource-id', 'resource-id-alternate']) assert ids == set(['resource-id', 'resource-id-alternate'])
def test_get_resources_by_metaquery(self):
q = {'metadata.display_name': 'test-server'}
got_not_imp = False
try:
list(self.conn.get_resources(metaquery=q))
except NotImplementedError:
got_not_imp = True
self.assertTrue(got_not_imp)
#this should work, but it doesn't.
#actually unless I wrap get_resources in list()
#it doesn't get called - weird
#self.assertRaises(NotImplementedError,
# self.conn.get_resources,
# metaquery=q)
class MeterTest(SQLAlchemyEngineTestBase): class MeterTest(SQLAlchemyEngineTestBase):
@ -336,16 +351,33 @@ class MeterTest(SQLAlchemyEngineTestBase):
def test_get_meters_by_project(self): def test_get_meters_by_project(self):
results = list(self.conn.get_meters(project='project-id')) results = list(self.conn.get_meters(project='project-id'))
for r in results:
print r
assert len(results) == 2 assert len(results) == 2
def test_get_meters_by_metaquery(self):
q = {'metadata.display_name': 'test-server'}
got_not_imp = False
try:
list(self.conn.get_meters(metaquery=q))
except NotImplementedError:
got_not_imp = True
self.assertTrue(got_not_imp)
def test_get_raw_events_by_user(self): def test_get_raw_events_by_user(self):
f = storage.EventFilter(user='user-id') f = storage.EventFilter(user='user-id')
results = list(self.conn.get_raw_events(f)) results = list(self.conn.get_raw_events(f))
assert len(results) == 2 assert len(results) == 2
self._iterate_msgs(results) self._iterate_msgs(results)
def test_get_events_by_metaquery(self):
q = {'metadata.display_name': 'test-server'}
f = storage.EventFilter(metaquery=q)
got_not_imp = False
try:
list(self.conn.get_raw_events(f))
except NotImplementedError:
got_not_imp = True
self.assertTrue(got_not_imp)
def test_get_raw_events_by_project(self): def test_get_raw_events_by_project(self):
f = storage.EventFilter(project='project-id') f = storage.EventFilter(project='project-id')
results = list(self.conn.get_raw_events(f)) results = list(self.conn.get_raw_events(f))