From 06a7b498b574a294a18574016f5ba2b8d4702516 Mon Sep 17 00:00:00 2001 From: Julien Danjou Date: Wed, 4 Dec 2013 17:53:50 +0100 Subject: [PATCH] Implement the /v2/sample 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 fields are also correctly named this time. Change-Id: I9e01a082c04e0462811befd98899e2f2d4a4dc82 Blueprint: sample-api --- ceilometer/api/controllers/v2.py | 100 ++++++++++++++++-- .../api/v2/test_list_meters_scenarios.py | 31 ++++++ 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/ceilometer/api/controllers/v2.py b/ceilometer/api/controllers/v2.py index 39ddd5d7c..4d4c39504 100644 --- a/ceilometer/api/controllers/v2.py +++ b/ceilometer/api/controllers/v2.py @@ -527,8 +527,10 @@ def _send_notification(event, payload): notification, notify.INFO, payload) -class Sample(_Base): +class OldSample(_Base): """A single measurement for a given meter and resource. + + This class is deprecated in favor of Sample. """ source = wtypes.text @@ -576,9 +578,9 @@ class Sample(_Base): if timestamp and isinstance(timestamp, basestring): timestamp = timeutils.parse_isotime(timestamp) - super(Sample, self).__init__(counter_volume=counter_volume, - resource_metadata=resource_metadata, - timestamp=timestamp, **kwds) + super(OldSample, self).__init__(counter_volume=counter_volume, + resource_metadata=resource_metadata, + timestamp=timestamp, **kwds) if self.resource_metadata in (wtypes.Unset, None): self.resource_metadata = {} @@ -706,7 +708,7 @@ class MeterController(rest.RestController): pecan.request.context['meter_id'] = meter_id self._id = meter_id - @wsme_pecan.wsexpose([Sample], [Query], int) + @wsme_pecan.wsexpose([OldSample], [Query], int) def get_all(self, q=[], limit=None): """Return samples for the meter. @@ -718,11 +720,11 @@ class MeterController(rest.RestController): kwargs = _query_to_kwargs(q, storage.SampleFilter.__init__) kwargs['meter'] = self._id f = storage.SampleFilter(**kwargs) - return [Sample.from_db_model(e) + return [OldSample.from_db_model(e) for e in pecan.request.storage_conn.get_samples(f, limit=limit) ] - @wsme_pecan.wsexpose([Sample], body=[Sample]) + @wsme_pecan.wsexpose([OldSample], body=[OldSample]) def post(self, samples): """Post a list of new Samples to Ceilometer. @@ -884,6 +886,89 @@ class MetersController(rest.RestController): for m in pecan.request.storage_conn.get_meters(**kwargs)] +class Sample(_Base): + """One measurement.""" + + id = wtypes.text + "The unique identifier for the sample." + + meter = wtypes.text + "The meter name this sample is for." + + type = wtypes.Enum(str, *sample.TYPES) + "The meter type (see :ref:`measurements`)" + + unit = wtypes.text + "The unit of measure." + + volume = float + "The metered value." + + user_id = wtypes.text + "The user this sample was taken for." + + project_id = wtypes.text + "The project this sample was taken for." + + resource_id = wtypes.text + "The :class:`Resource` this sample was taken for." + + source = wtypes.text + "The source that identifies where the sample comes from." + + timestamp = datetime.datetime + "When the sample has been generated." + + metadata = {wtypes.text: wtypes.text} + "Arbitrary metadata associated with the sample." + + @classmethod + def from_db_model(cls, m): + return cls(id=m.message_id, + meter=m.counter_name, + type=m.counter_type, + unit=m.counter_unit, + volume=m.counter_volume, + user_id=m.user_id, + project_id=m.project_id, + resource_id=m.resource_id, + timestamp=m.timestamp, + metadata=_flatten_metadata(m.resource_metadata)) + + @classmethod + def sample(cls): + return cls(id=str(uuid.uuid1()), + meter='instance', + type='gauge', + unit='instance', + resource_id='bd9431c1-8d69-4ad3-803a-8d4a6b89fd36', + project_id='35b17138-b364-4e6a-a131-8f3099c5be68', + user_id='efd87807-12d2-4b38-9c70-5f5c2ac427ff', + timestamp=timeutils.utcnow(), + source='openstack', + metadata={'name1': 'value1', + 'name2': 'value2'}, + ) + + +class SamplesController(rest.RestController): + """Controller managing the samples.""" + + @wsme_pecan.wsexpose([Sample], [Query], int) + def get_all(self, q=[], limit=None): + """Return all known samples, based on the data recorded so far. + + :param q: Filter rules for the samples to be returned. + :param limit: Maximum number of samples to be returned. + """ + if limit and limit < 0: + raise ClientSideError(_("Limit must be positive")) + kwargs = _query_to_kwargs(q, storage.SampleFilter.__init__) + f = storage.SampleFilter(**kwargs) + return map(Sample.from_db_model, + pecan.request.storage_conn.get_samples(f, limit=limit)) + + class Resource(_Base): """An externally defined object for which samples have been received. """ @@ -1511,4 +1596,5 @@ class V2Controller(object): resources = ResourcesController() meters = MetersController() + samples = SamplesController() alarms = AlarmsController() diff --git a/ceilometer/tests/api/v2/test_list_meters_scenarios.py b/ceilometer/tests/api/v2/test_list_meters_scenarios.py index 52351a3ec..9e5747934 100644 --- a/ceilometer/tests/api/v2/test_list_meters_scenarios.py +++ b/ceilometer/tests/api/v2/test_list_meters_scenarios.py @@ -146,6 +146,10 @@ class TestListMeters(FunctionalTest, self.assertEqual(set(r['source'] for r in data), set(['test_source', 'test_source1'])) + def test_list_samples(self): + data = self.get_json('/samples') + self.assertEqual(5, len(data)) + def test_list_meters_with_dict_metadata(self): data = self.get_json('/meters/meter.mine', q=[{'field': @@ -160,6 +164,33 @@ class TestListMeters(FunctionalTest, self.assertEqual('self.sample4', metadata['tag']) self.assertEqual('prop_value', metadata['properties.prop_1']) + def test_list_samples_with_dict_metadata(self): + data = self.get_json('/samples', + q=[{'field': + 'metadata.properties.prop_2.sub_prop_1', + 'op': 'eq', + 'value': 'sub_prop_value', + }]) + self.assertTrue('id' in data[0]) + del data[0]['id'] # Randomly generated + self.assertEqual(data, [{ + u'user_id': u'user-id4', + u'resource_id': u'resource-id4', + u'timestamp': u'2012-07-02T10:43:00', + u'meter': u'meter.mine', + u'volume': 1.0, + u'project_id': u'project-id2', + u'type': u'gauge', + u'unit': u'', + u'metadata': {u'display_name': u'test-server', + u'properties.prop_2:sub_prop_1': u'sub_prop_value', + u'util': u'0.58', + u'tag': u'self.sample4', + u'properties.prop_1': u'prop_value', + u'is_public': u'True', + u'size': u'0'} + }]) + def test_list_meters_metadata_query(self): data = self.get_json('/meters/meter.test', q=[{'field': 'metadata.tag',