From bfbf344562d8a229baeeaf8673bc0c559cb0a994 Mon Sep 17 00:00:00 2001 From: Steve Lewis Date: Mon, 11 Aug 2014 15:46:00 -0700 Subject: [PATCH] Add support for Samples coming from Ceilometer Can add or list Samples only. Samples are a sub-resource under a Metric. Query-String filtering of samples is supported. The common Resource type is changed to allow aliasing of resource attribute names, required because Sample has two published and supported resource formats within a single API version. Change-Id: Ia1f157bffcd74dde203488917f2d5fa1df7e44aa --- openstack/resource.py | 24 +++- openstack/telemetry/v2/sample.py | 59 +++++++++ openstack/tests/telemetry/v2/test_sample.py | 138 ++++++++++++++++++++ openstack/tests/test_resource.py | 24 ++++ 4 files changed, 242 insertions(+), 3 deletions(-) create mode 100644 openstack/telemetry/v2/sample.py create mode 100644 openstack/tests/telemetry/v2/test_sample.py diff --git a/openstack/resource.py b/openstack/resource.py index 15e467b0..be730df0 100644 --- a/openstack/resource.py +++ b/openstack/resource.py @@ -58,17 +58,32 @@ class prop(object): >>> u = User() >>> u.age = 'thirty' TypeError: Invalid type for attr age + + + By specifying an alias attribute name, that alias will be read when the + primary attribute name does not appear within the resource: + + >>> class User(Resource): + ... name = prop('address', alias='location') + ... + >>> u = User(location='Far Away') + >>> print u['address'] + Far Away """ - def __init__(self, name, type=None): + def __init__(self, name, alias=None, type=None): self.name = name self.type = type + self.alias = alias def __get__(self, instance, owner): try: return instance._attrs[self.name] except KeyError: - raise AttributeError('Unset property: %s' % self.name) + try: + return instance._attrs[self.alias] + except KeyError: + raise AttributeError('Unset property: %s' % self.name) def __set__(self, instance, value): if self.type and not isinstance(value, self.type): @@ -80,7 +95,10 @@ class prop(object): try: del instance._attrs[self.name] except KeyError: - raise AttributeError('Unset property: %s' % self.name) + try: + del instance._attrs[self.alias] + except KeyError: + raise AttributeError('Unset property: %s' % self.name) @six.add_metaclass(abc.ABCMeta) diff --git a/openstack/telemetry/v2/sample.py b/openstack/telemetry/v2/sample.py new file mode 100644 index 00000000..21fbe478 --- /dev/null +++ b/openstack/telemetry/v2/sample.py @@ -0,0 +1,59 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from openstack import resource +from openstack.telemetry import telemetry_service + + +class Sample(resource.Resource): + id_attribute = 'sample_id' + base_path = '/v2/meters/%(meter)s' + service = telemetry_service.TelemetryService() + + # Supported Operations + allow_create = True + allow_list = True + + # Properties + metadata = resource.prop('metadata', alias='resource_metadata') + meter = resource.prop('meter', alias='counter_name') + project_id = resource.prop('project_id') + recorded_at = resource.prop('recorded_at') + resource_id = resource.prop('resource_id') + sample_id = resource.prop('id', alias='message_id') + source = resource.prop('source') + generated_at = resource.prop('timestamp') + type = resource.prop('type', alias='counter_type') + unit = resource.prop('unit', alias='counter_unit') + user_id = resource.prop('user_id') + volume = resource.prop('volume', alias='counter_volume') + + def __repr__(self): + return "sample: %s" % self._attrs + + @classmethod + def list(cls, session, path_args=None, **params): + url = cls.base_path % path_args + resp = session.get(url, service=cls.service, params=params) + + changes = [] + for item in resp.body: + changes.append(cls.existing(**item)) + return changes + + def create(self, session): + url = self.base_path % {'meter': self.meter} + # telemetry expects a list of samples + resp = session.post(url, service=self.service, json=[self._attrs]) + + sample = self.existing(**resp.body.pop()) + self._attrs['id'] = sample.id + self._reset_dirty() diff --git a/openstack/tests/telemetry/v2/test_sample.py b/openstack/tests/telemetry/v2/test_sample.py new file mode 100644 index 00000000..1f3a492a --- /dev/null +++ b/openstack/tests/telemetry/v2/test_sample.py @@ -0,0 +1,138 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock +import testtools + +from openstack.telemetry.v2 import sample + +SAMPLE = { + 'id': None, + 'metadata': {'1': 'one'}, + 'meter': '2', + 'project_id': '3', + 'recorded_at': '4', + 'resource_id': '5', + 'source': '6', + 'timestamp': '7', + 'type': '8', + 'unit': '9', + 'user_id': '10', + 'volume': '11.1', +} + +OLD_SAMPLE = { + 'counter_name': '1', + 'counter_type': '2', + 'counter_unit': '3', + 'counter_volume': '4', + 'message_id': None, + 'project_id': '5', + 'recorded_at': '6', + 'resource_id': '7', + 'resource_metadata': '8', + 'source': '9', + 'timestamp': '10', + 'user_id': '11', +} + + +class TestSample(testtools.TestCase): + + def test_basic(self): + sot = sample.Sample(SAMPLE) + self.assertIsNone(sot.resource_key) + self.assertIsNone(sot.resources_key) + self.assertEqual('/v2/meters/%(meter)s', sot.base_path) + self.assertEqual('metering', sot.service.service_type) + self.assertTrue(sot.allow_create) + self.assertFalse(sot.allow_retrieve) + self.assertFalse(sot.allow_update) + self.assertFalse(sot.allow_delete) + self.assertTrue(sot.allow_list) + + def test_make_new(self): + sot = sample.Sample(SAMPLE) + self.assertIsNone(sot.id) + self.assertEqual(SAMPLE['metadata'], sot.metadata) + self.assertEqual(SAMPLE['meter'], sot.meter) + self.assertEqual(SAMPLE['project_id'], sot.project_id) + self.assertEqual(SAMPLE['recorded_at'], sot.recorded_at) + self.assertEqual(SAMPLE['resource_id'], sot.resource_id) + self.assertIsNone(sot.sample_id) + self.assertEqual(SAMPLE['source'], sot.source) + self.assertEqual(SAMPLE['timestamp'], sot.generated_at) + self.assertEqual(SAMPLE['type'], sot.type) + self.assertEqual(SAMPLE['unit'], sot.unit) + self.assertEqual(SAMPLE['user_id'], sot.user_id) + self.assertEqual(SAMPLE['volume'], sot.volume) + + def test_make_old(self): + sot = sample.Sample(OLD_SAMPLE) + self.assertIsNone(sot.id) + self.assertIsNone(sot.sample_id), + self.assertEqual(OLD_SAMPLE['counter_name'], sot.meter) + self.assertEqual(OLD_SAMPLE['counter_type'], sot.type) + self.assertEqual(OLD_SAMPLE['counter_unit'], sot.unit) + self.assertEqual(OLD_SAMPLE['counter_volume'], sot.volume) + self.assertEqual(OLD_SAMPLE['project_id'], sot.project_id) + self.assertEqual(OLD_SAMPLE['recorded_at'], sot.recorded_at) + self.assertEqual(OLD_SAMPLE['resource_id'], sot.resource_id) + self.assertEqual(OLD_SAMPLE['resource_metadata'], sot.metadata) + self.assertEqual(OLD_SAMPLE['source'], sot.source) + self.assertEqual(OLD_SAMPLE['timestamp'], sot.generated_at) + self.assertEqual(OLD_SAMPLE['user_id'], sot.user_id) + + def test_list(self): + sess = mock.Mock() + resp = mock.Mock() + resp.body = [SAMPLE, OLD_SAMPLE] + sess.get = mock.Mock(return_value=resp) + path_args = {'meter': 'name_of_meter'} + + found = sample.Sample.list(sess, path_args=path_args) + self.assertEqual(2, len(found)) + first = found[0] + self.assertIsNone(first.id) + self.assertIsNone(first.sample_id) + self.assertEqual(SAMPLE['metadata'], first.metadata) + self.assertEqual(SAMPLE['meter'], first.meter) + self.assertEqual(SAMPLE['project_id'], first.project_id) + self.assertEqual(SAMPLE['recorded_at'], first.recorded_at) + self.assertEqual(SAMPLE['resource_id'], first.resource_id) + self.assertEqual(SAMPLE['source'], first.source) + self.assertEqual(SAMPLE['timestamp'], first.generated_at) + self.assertEqual(SAMPLE['type'], first.type) + self.assertEqual(SAMPLE['unit'], first.unit) + self.assertEqual(SAMPLE['user_id'], first.user_id) + self.assertEqual(SAMPLE['volume'], first.volume) + + def test_create(self): + sess = mock.Mock() + resp = mock.Mock() + resp.body = [SAMPLE] + sess.post = mock.Mock(return_value=resp) + + data = {'id': None, + 'meter': 'temperature', + 'project_id': 'project', + 'resource_id': 'resource', + 'type': 'gauge', + 'unit': 'instance', + 'volume': '98.6'} + new_sample = sample.Sample.new(**data) + + new_sample.create(sess) + url = '/v2/meters/temperature' + sess.post.assert_called_with(url, service=new_sample.service, + json=[data]) + self.assertIsNone(new_sample.id) \ No newline at end of file diff --git a/openstack/tests/test_resource.py b/openstack/tests/test_resource.py index 3b053db9..b25e698b 100644 --- a/openstack/tests/test_resource.py +++ b/openstack/tests/test_resource.py @@ -52,6 +52,7 @@ class FakeResource(resource.Resource): name = resource.prop('name') first = resource.prop('attr1') second = resource.prop('attr2') + third = resource.prop('attr3', alias='attr_three') class ResourceTests(base.TestTransportBase): @@ -229,6 +230,29 @@ class ResourceTests(base.TestTransportBase): else: self.fail("Didn't raise attribute error") + try: + obj.third + except AttributeError: + pass + else: + self.fail("Didn't raise attribute error") + + def test_composite_attr_happy(self): + obj = FakeResource.existing(**{'attr3': '3'}) + + try: + self.assertEqual('3', obj.third) + except AttributeError: + self.fail("third was not found as expected") + + def test_composite_attr_fallback(self): + obj = FakeResource.existing(**{'attr_three': '3'}) + + try: + self.assertEqual('3', obj.third) + except AttributeError: + self.fail("third was not found in fallback as expected") + class FakeResponse: def __init__(self, response):