From 12abf68a922994a18c8123c49932f44c855d5410 Mon Sep 17 00:00:00 2001 From: Mehdi Abaakouk Date: Thu, 12 Dec 2013 17:43:42 +0100 Subject: [PATCH] Implement the /v2/samples/ API This is a simple implementation of the new sample endpoint in the API. It provides raw accesses to the samples collected by Ceilometer, without imposing any kind of filtering on the meter name. The patch is about retreiving only one sample with the id. The fields are also correctly named this time. Change-Id: Ifdd7f024b066407cd42ec0572385f97acd312125 Blueprint: sample-api --- ceilometer/api/controllers/v2.py | 14 ++++++++ ceilometer/storage/__init__.py | 5 ++- ceilometer/storage/impl_db2.py | 2 ++ ceilometer/storage/impl_hbase.py | 9 +++++- ceilometer/storage/impl_mongodb.py | 2 ++ ceilometer/storage/impl_sqlalchemy.py | 2 ++ .../api/v2/test_list_meters_scenarios.py | 32 ++++++++++++++++++- 7 files changed, 63 insertions(+), 3 deletions(-) diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index 74c7f20bd..c59c72baf 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -973,6 +973,20 @@ class SamplesController(rest.RestController): return map(Sample.from_db_model, pecan.request.storage_conn.get_samples(f, limit=limit)) + @wsme_pecan.wsexpose(Sample, wtypes.text) + def get_one(self, sample_id): + """Return a sample + + :param sample_id: the id of the sample + """ + f = storage.SampleFilter(message_id=sample_id) + + samples = list(pecan.request.storage_conn.get_samples(f)) + if len(samples) < 1: + raise EntityNotFound(_('Sample'), sample_id) + + return Sample.from_db_model(samples[0]) + class Resource(_Base): """An externally defined object for which samples have been received. diff --git a/ceilometer/storage/__init__.py b/ceilometer/storage/__init__.py index 8418b5d41..318af1d57 100644 --- a/ceilometer/storage/__init__.py +++ b/ceilometer/storage/__init__.py @@ -95,13 +95,15 @@ class SampleFilter(object): :param resource: Optional filter for resource id. :param meter: Optional filter for meter type using the meter name. :param source: Optional source filter. + :param message_id: Optional sample_id filter. :param metaquery: Optional filter on the metadata """ def __init__(self, user=None, project=None, start=None, start_timestamp_op=None, end=None, end_timestamp_op=None, resource=None, meter=None, - source=None, metaquery={}): + source=None, message_id=None, + metaquery={}): self.user = user self.project = project self.start = utils.sanitize_timestamp(start) @@ -112,6 +114,7 @@ class SampleFilter(object): self.meter = meter self.source = source self.metaquery = metaquery + self.message_id = message_id class EventFilter(object): diff --git a/ceilometer/storage/impl_db2.py b/ceilometer/storage/impl_db2.py index 5d3276fc5..631cd1d85 100644 --- a/ceilometer/storage/impl_db2.py +++ b/ceilometer/storage/impl_db2.py @@ -128,6 +128,8 @@ def make_query_from_filter(sample_filter, require_meter=True): q['resource_id'] = sample_filter.resource if sample_filter.source: q['source'] = sample_filter.source + if sample_filter.message_id: + q['message_id'] = sample_filter.message_id # so the samples call metadata resource_metadata, so we convert # to that. diff --git a/ceilometer/storage/impl_hbase.py b/ceilometer/storage/impl_hbase.py index 349c375a4..b4d85f21d 100644 --- a/ceilometer/storage/impl_hbase.py +++ b/ceilometer/storage/impl_hbase.py @@ -241,6 +241,7 @@ class Connection(base.Connection): # TODO(shengjie) extra dimensions need to be added as CQ 'f:user_id': data['user_id'], 'f:project_id': data['project_id'], + 'f:message_id': data['message_id'], 'f:resource_id': data['resource_id'], 'f:source': data['source'], # add in reversed_ts here for time range scan @@ -707,7 +708,8 @@ def reverse_timestamp(dt): def make_query(user=None, project=None, meter=None, resource=None, source=None, start=None, start_op=None, - end=None, end_op=None, require_meter=True, query_only=False): + end=None, end_op=None, message_id=None, require_meter=True, + query_only=False): """Return a filter query string based on the selected parameters. :param user: Optional user-id @@ -719,6 +721,7 @@ def make_query(user=None, project=None, meter=None, :param start_op: Optional start timestamp operator, like gt, ge :param end: Optional end timestamp :param end_op: Optional end timestamp operator, like lt, le + :param message_id: Optional message_id :param require_meter: If true and the filter does not have a meter, raise an error. :param query_only: If true only returns the filter query, @@ -735,6 +738,9 @@ def make_query(user=None, project=None, meter=None, if resource: q.append("SingleColumnValueFilter ('f', 'resource_id', =, 'binary:%s')" % resource) + if message_id: + q.append("SingleColumnValueFilter ('f', 'message_id', =, 'binary:%s')" + % message_id) if source: q.append("SingleColumnValueFilter " "('f', 'source', =, 'binary:%s')" % source) @@ -792,6 +798,7 @@ def make_query_from_filter(sample_filter, require_meter=True): sample_filter.start_timestamp_op, sample_filter.end, sample_filter.end_timestamp_op, + sample_filter.message_id, require_meter) diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index ec41879b7..7ca143838 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -131,6 +131,8 @@ def make_query_from_filter(sample_filter, require_meter=True): q['resource_id'] = sample_filter.resource if sample_filter.source: q['source'] = sample_filter.source + if sample_filter.message_id: + q['message_id'] = sample_filter.message_id # so the samples call metadata resource_metadata, so we convert # to that. diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index 8cd455d71..310ff8fea 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -159,6 +159,8 @@ def make_query_from_filter(session, query, sample_filter, require_meter=True): query = query.filter_by(project_id=sample_filter.project) if sample_filter.resource: query = query.filter_by(resource_id=sample_filter.resource) + if sample_filter.message_id: + query = query.filter_by(message_id=sample_filter.message_id) if sample_filter.metaquery: query = apply_metaquery_filter(session, query, diff --git a/ceilometer/tests/api/v2/test_list_meters_scenarios.py b/ceilometer/tests/api/v2/test_list_meters_scenarios.py index 067cc39ef..7aeb51b87 100644 --- a/ceilometer/tests/api/v2/test_list_meters_scenarios.py +++ b/ceilometer/tests/api/v2/test_list_meters_scenarios.py @@ -21,6 +21,7 @@ import base64 import datetime +import json as jsonutils import logging import testscenarios @@ -47,6 +48,7 @@ class TestListMeters(FunctionalTest, def setUp(self): super(TestListMeters, self).setUp() + self.messages = [] for cnt in [ sample.Sample( 'meter.test', @@ -131,6 +133,7 @@ class TestListMeters(FunctionalTest, msg = utils.meter_message_from_counter( cnt, self.CONF.publisher.metering_secret) + self.messages.append(msg) self.conn.record_metering_data(msg) def test_list_meters(self): @@ -164,6 +167,33 @@ class TestListMeters(FunctionalTest, self.assertEqual('self.sample4', metadata['tag']) self.assertEqual('prop_value', metadata['properties.prop_1']) + def test_get_one_sample(self): + sample_id = self.messages[1]['message_id'] + data = self.get_json('/samples/%s' % sample_id) + self.assertIn('id', data) + self.assertEqual(data, { + u'id': sample_id, + u'metadata': {u'display_name': u'test-server', + u'is_public': u'False', + u'size': u'0', + u'tag': u'self.sample1', + u'util': u'0.47'}, + u'meter': u'meter.test', + u'project_id': u'project-id', + u'resource_id': u'resource-id', + u'timestamp': u'2012-07-02T11:40:00', + u'type': u'cumulative', + u'unit': u'', + u'user_id': u'user-id', + u'volume': 3.0}) + + def test_get_not_existing_sample(self): + resp = self.get_json('/samples/not_exists', expect_errors=True) + self.assertEqual(resp.status_code, 404) + self.assertEqual(jsonutils.loads(resp.body)['error_message'] + ['faultstring'], + "Sample not_exists Not Found") + def test_list_samples_with_dict_metadata(self): data = self.get_json('/samples', q=[{'field': @@ -171,7 +201,7 @@ class TestListMeters(FunctionalTest, 'op': 'eq', 'value': 'sub_prop_value', }]) - self.assertTrue('id' in data[0]) + self.assertIn('id', data[0]) del data[0]['id'] # Randomly generated self.assertEqual(data, [{ u'user_id': u'user-id4',