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',