From 1831bc1160f65e13e1bc0a8b72bbe2a1f4c8dfd0 Mon Sep 17 00:00:00 2001 From: ZhiQiang Fan Date: Sun, 2 Nov 2014 10:25:19 +0800 Subject: Add Sample API support Sample API has been implemented in Ceilometer for a long time, but CLI is lack of such support, this patch implements Sample CLI. Implements blueprint cli-samples-api Change-Id: I67152c636526dad3ec27e06058ff73ad969ae2b9 DocImpact --- ceilometerclient/common/utils.py | 3 +- ceilometerclient/tests/v2/test_samples.py | 147 +++++++++++++++++++------ ceilometerclient/tests/v2/test_shell.py | 172 ++++++++++++++++++++++++------ ceilometerclient/v2/client.py | 3 +- ceilometerclient/v2/samples.py | 40 ++++++- ceilometerclient/v2/shell.py | 45 +++++++- requirements.txt | 1 + 7 files changed, 338 insertions(+), 73 deletions(-) diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index b5bbe79..4e77632 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -19,6 +19,7 @@ import sys import textwrap import uuid +from oslo.serialization import jsonutils from oslo.utils import encodeutils from oslo.utils import importutils import prettytable @@ -89,7 +90,7 @@ def print_dict(d, dict_property="Property", wrap=0): for k, v in sorted(six.iteritems(d)): # convert dict to str to check length if isinstance(v, dict): - v = str(v) + v = jsonutils.dumps(v) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, six.string_types) and r'\n' in v: diff --git a/ceilometerclient/tests/v2/test_samples.py b/ceilometerclient/tests/v2/test_samples.py index dfbdf39..aee2220 100644 --- a/ceilometerclient/tests/v2/test_samples.py +++ b/ceilometerclient/tests/v2/test_samples.py @@ -20,62 +20,103 @@ from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.samples -GET_SAMPLE = {u'counter_name': u'instance', - u'user_id': u'user-id', - u'resource_id': u'resource-id', - u'timestamp': u'2012-07-02T10:40:00', - u'source': u'test_source', - u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f', - u'counter_unit': u'', - u'counter_volume': 1.0, - u'project_id': u'project1', - u'resource_metadata': {u'tag': u'self.counter', - u'display_name': u'test-server'}, - u'counter_type': u'cumulative'} -CREATE_SAMPLE = copy.deepcopy(GET_SAMPLE) +GET_OLD_SAMPLE = {u'counter_name': u'instance', + u'user_id': u'user-id', + u'resource_id': u'resource-id', + u'timestamp': u'2012-07-02T10:40:00', + u'source': u'test_source', + u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f', + u'counter_unit': u'', + u'counter_volume': 1.0, + u'project_id': u'project1', + u'resource_metadata': {u'tag': u'self.counter', + u'display_name': u'test-server'}, + u'counter_type': u'cumulative'} +CREATE_SAMPLE = copy.deepcopy(GET_OLD_SAMPLE) del CREATE_SAMPLE['message_id'] del CREATE_SAMPLE['source'] -base_url = '/v2/meters/instance' -args = ('q.field=resource_id&q.field=source&q.op=&q.op=' - '&q.type=&q.type=&q.value=foo&q.value=bar') -args_limit = 'limit=1' -fixtures = { - base_url: - { +GET_SAMPLE = { + "user_id": None, + "resource_id": "9b651dfd-7d30-402b-972e-212b2c4bfb05", + "timestamp": "2014-11-03T13:37:46", + "meter": "image", + "volume": 1.0, + "source": "openstack", + "recorded_at": "2014-11-03T13:37:46.994458", + "project_id": "2cc3a7bb859b4bacbeab0aa9ca673033", + "type": "gauge", + "id": "98b5f258-635e-11e4-8bdd-0025647390c1", + "unit": "image", + "resource_metadata": {}, +} + +METER_URL = '/v2/meters/instance' +SAMPLE_URL = '/v2/samples' +QUERIES = ('q.field=resource_id&q.field=source&q.op=&q.op=' + '&q.type=&q.type=&q.value=foo&q.value=bar') +LIMIT = 'limit=1' + +OLD_SAMPLE_FIXTURES = { + METER_URL: { 'GET': ( {}, - [GET_SAMPLE] + [GET_OLD_SAMPLE] ), 'POST': ( {}, [CREATE_SAMPLE], ), }, - '%s?%s' % (base_url, args): - { + '%s?%s' % (METER_URL, QUERIES): { 'GET': ( {}, [], ), }, - '%s?%s' % (base_url, args_limit): - { + '%s?%s' % (METER_URL, LIMIT): { 'GET': ( {}, - [GET_SAMPLE] + [GET_OLD_SAMPLE] ), } } +SAMPLE_FIXTURES = { + SAMPLE_URL: { + 'GET': ( + (), + [GET_SAMPLE] + ), + }, + '%s?%s' % (SAMPLE_URL, QUERIES): { + 'GET': ( + {}, + [], + ), + }, + '%s?%s' % (SAMPLE_URL, LIMIT): { + 'GET': ( + {}, + [GET_SAMPLE], + ), + }, + '%s/%s' % (SAMPLE_URL, GET_SAMPLE['id']): { + 'GET': ( + {}, + GET_SAMPLE, + ), + }, +} -class SampleManagerTest(utils.BaseTestCase): +class OldSampleManagerTest(utils.BaseTestCase): def setUp(self): - super(SampleManagerTest, self).setUp() - self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + super(OldSampleManagerTest, self).setUp() + self.http_client = fake_client.FakeHTTPClient( + fixtures=OLD_SAMPLE_FIXTURES) self.api = client.BaseClient(self.http_client) - self.mgr = ceilometerclient.v2.samples.SampleManager(self.api) + self.mgr = ceilometerclient.v2.samples.OldSampleManager(self.api) def test_list_by_meter_name(self): samples = list(self.mgr.list(meter_name='instance')) @@ -94,7 +135,7 @@ class SampleManagerTest(utils.BaseTestCase): {"field": "source", "value": "bar"}, ])) - expect = ['GET', '%s?%s' % (base_url, args)] + expect = ['GET', '%s?%s' % (METER_URL, QUERIES)] self.http_client.assert_called(*expect) self.assertEqual(len(samples), 0) @@ -111,3 +152,47 @@ class SampleManagerTest(utils.BaseTestCase): expect = ['GET', '/v2/meters/instance?limit=1'] self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) + + +class SampleManagerTest(utils.BaseTestCase): + + def setUp(self): + super(SampleManagerTest, self).setUp() + self.http_client = fake_client.FakeHTTPClient( + fixtures=SAMPLE_FIXTURES) + self.api = client.BaseClient(self.http_client) + self.mgr = ceilometerclient.v2.samples.SampleManager(self.api) + + def test_sample_list(self): + samples = list(self.mgr.list()) + expect = [ + 'GET', '/v2/samples' + ] + self.http_client.assert_called(*expect) + self.assertEqual(1, len(samples)) + self.assertEqual('9b651dfd-7d30-402b-972e-212b2c4bfb05', + samples[0].resource_id) + + def test_sample_list_with_queries(self): + queries = [ + {"field": "resource_id", + "value": "foo"}, + {"field": "source", + "value": "bar"}, + ] + samples = list(self.mgr.list(q=queries)) + expect = ['GET', '%s?%s' % (SAMPLE_URL, QUERIES)] + self.http_client.assert_called(*expect) + self.assertEqual(0, len(samples)) + + def test_sample_list_with_limit(self): + samples = list(self.mgr.list(limit=1)) + expect = ['GET', '/v2/samples?limit=1'] + self.http_client.assert_called(*expect) + self.assertEqual(1, len(samples)) + + def test_sample_get(self): + sample = self.mgr.get(GET_SAMPLE['id']) + expect = ['GET', '/v2/samples/' + GET_SAMPLE['id']] + self.http_client.assert_called(*expect) + self.assertEqual(GET_SAMPLE, sample.to_dict()) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 65c8bb1..fcd2d6a 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -324,43 +324,70 @@ class ShellAlarmCommandTest(utils.BaseTestCase): class ShellSampleListCommandTest(utils.BaseTestCase): METER = 'cpu_util' - SAMPLES = [{"counter_name": "cpu_util", - "resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", - "timestamp": "2013-10-15T05:50:30", - "counter_unit": "%", - "counter_volume": 0.261666666667, - "counter_type": "gauge"}, - {"counter_name": "cpu_util", - "resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", - "timestamp": "2013-10-15T05:50:29", - "counter_unit": "%", - "counter_volume": 0.261666666667, - "counter_type": "gauge"}, - {"counter_name": "cpu_util", - "resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", - "timestamp": "2013-10-15T05:40:30", - "counter_unit": "%", - "counter_volume": 0.251247920133, - "counter_type": "gauge"}, - {"counter_name": "cpu_util", - "resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", - "timestamp": "2013-10-15T05:40:29", - "counter_unit": "%", - "counter_volume": 0.26, - "counter_type": "gauge"}] + SAMPLE_VALUES = ( + ("cpu_util", + "5dcf5537-3161-4e25-9235-407e1385bd35", + "2013-10-15T05:50:30", + "%", + 0.261666666667, + "gauge", + "86536501-b2c9-48f6-9c6a-7a5b14ba7482"), + ("cpu_util", + "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", + "2013-10-15T05:50:29", + "%", + 0.261666666667, + "gauge", + "fe2a91ec-602b-4b55-8cba-5302ce3b916e",), + ("cpu_util", + "5dcf5537-3161-4e25-9235-407e1385bd35", + "2013-10-15T05:40:30", + "%", + 0.251247920133, + "gauge", + "52768bcb-b4e9-4db9-a30c-738c758b6f43"), + ("cpu_util", + "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", + "2013-10-15T05:40:29", + "%", + 0.26, + "gauge", + "31ae614a-ac6b-4fb9-b106-4667bae03308"), + ) + + OLD_SAMPLES = [ + dict(counter_name=s[0], + resource_id=s[1], + timestamp=s[2], + counter_unit=s[3], + counter_volume=s[4], + counter_type=s[5]) + for s in SAMPLE_VALUES + ] + + SAMPLES = [ + dict(meter=s[0], + resource_id=s[1], + timestamp=s[2], + unit=s[3], + volume=s[4], + type=s[5], + id=s[6]) + for s in SAMPLE_VALUES + ] def setUp(self): super(ShellSampleListCommandTest, self).setUp() self.cc = mock.Mock() self.args = mock.Mock() - self.args.meter = self.METER self.args.query = None self.args.limit = None @mock.patch('sys.stdout', new=six.StringIO()) - def test_sample_list(self): - sample_list = [samples.Sample(mock.Mock(), sample) - for sample in self.SAMPLES] + def test_old_sample_list(self): + self.args.meter = self.METER + sample_list = [samples.OldSample(mock.Mock(), sample) + for sample in self.OLD_SAMPLES] self.cc.samples.list.return_value = sample_list ceilometer_shell.do_sample_list(self.cc, self.args) @@ -388,6 +415,91 @@ class ShellSampleListCommandTest(utils.BaseTestCase): +------+---------------------+ ''', sys.stdout.getvalue()) + @mock.patch('sys.stdout', new=six.StringIO()) + def test_sample_list(self): + self.args.meter = None + sample_list = [samples.Sample(mock.Mock(), sample) + for sample in self.SAMPLES] + self.cc.new_samples.list.return_value = sample_list + + ceilometer_shell.do_sample_list(self.cc, self.args) + self.cc.new_samples.list.assert_called_once_with( + q=None, + limit=None) + + self.assertEqual('''\ ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +| ID | Resource ID \ +| Name | Type | Volume | Unit | Timestamp | ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +| 86536501-b2c9-48f6-9c6a-7a5b14ba7482 | 5dcf5537-3161-4e25-9235-407e1385bd35 \ +| cpu_util | gauge | 0.261666666667 | % | 2013-10-15T05:50:30 | +| fe2a91ec-602b-4b55-8cba-5302ce3b916e | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f \ +| cpu_util | gauge | 0.261666666667 | % | 2013-10-15T05:50:29 | +| 52768bcb-b4e9-4db9-a30c-738c758b6f43 | 5dcf5537-3161-4e25-9235-407e1385bd35 \ +| cpu_util | gauge | 0.251247920133 | % | 2013-10-15T05:40:30 | +| 31ae614a-ac6b-4fb9-b106-4667bae03308 | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f \ +| cpu_util | gauge | 0.26 | % | 2013-10-15T05:40:29 | ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +''', sys.stdout.getvalue()) + + +class ShellSampleShowCommandTest(utils.BaseTestCase): + + SAMPLE = { + "user_id": None, + "resource_id": "9b651dfd-7d30-402b-972e-212b2c4bfb05", + "timestamp": "2014-11-03T13:37:46", + "meter": "image", + "volume": 1.0, + "source": "openstack", + "recorded_at": "2014-11-03T13:37:46.994458", + "project_id": "2cc3a7bb859b4bacbeab0aa9ca673033", + "type": "gauge", + "id": "98b5f258-635e-11e4-8bdd-0025647390c1", + "unit": "image", + "metadata": { + "name": "cirros-0.3.2-x86_64-uec", + } + } + + def setUp(self): + super(ShellSampleShowCommandTest, self).setUp() + self.cc = mock.Mock() + self.args = mock.Mock() + self.args.sample_id = "98b5f258-635e-11e4-8bdd-0025647390c1" + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_sample_show(self): + sample = samples.Sample(mock.Mock(), self.SAMPLE) + self.cc.samples.get.return_value = sample + + ceilometer_shell.do_sample_show(self.cc, self.args) + self.cc.samples.get.assert_called_once_with( + "98b5f258-635e-11e4-8bdd-0025647390c1") + + self.assertEqual('''\ ++-------------+--------------------------------------+ +| Property | Value | ++-------------+--------------------------------------+ +| id | 98b5f258-635e-11e4-8bdd-0025647390c1 | +| metadata | {"name": "cirros-0.3.2-x86_64-uec"} | +| meter | image | +| project_id | 2cc3a7bb859b4bacbeab0aa9ca673033 | +| recorded_at | 2014-11-03T13:37:46.994458 | +| resource_id | 9b651dfd-7d30-402b-972e-212b2c4bfb05 | +| source | openstack | +| timestamp | 2014-11-03T13:37:46 | +| type | gauge | +| unit | image | +| user_id | None | +| volume | 1.0 | ++-------------+--------------------------------------+ +''', sys.stdout.getvalue()) + class ShellSampleCreateCommandTest(utils.BaseTestCase): @@ -422,9 +534,9 @@ class ShellSampleCreateCommandTest(utils.BaseTestCase): @mock.patch('sys.stdout', new=six.StringIO()) def test_sample_create(self): - ret_sample = [samples.Sample(mock.Mock(), sample) + ret_sample = [samples.OldSample(mock.Mock(), sample) for sample in self.SAMPLE] - self.cc.samples.create.return_value = ret_sample + self.cc.old_samples.create.return_value = ret_sample ceilometer_shell.do_sample_create(self.cc, self.args) diff --git a/ceilometerclient/v2/client.py b/ceilometerclient/v2/client.py index 19f847d..a4a3619 100644 --- a/ceilometerclient/v2/client.py +++ b/ceilometerclient/v2/client.py @@ -63,7 +63,8 @@ class Client(object): self.http_client = client.BaseClient(self.client) self.meters = meters.MeterManager(self.http_client) - self.samples = samples.SampleManager(self.http_client) + self.samples = samples.OldSampleManager(self.http_client) + self.new_samples = samples.SampleManager(self.http_client) self.statistics = statistics.StatisticsManager(self.http_client) self.resources = resources.ResourceManager(self.http_client) self.alarms = alarms.AlarmManager(self.http_client) diff --git a/ceilometerclient/v2/samples.py b/ceilometerclient/v2/samples.py index 8081efc..c046051 100644 --- a/ceilometerclient/v2/samples.py +++ b/ceilometerclient/v2/samples.py @@ -26,13 +26,18 @@ CREATION_ATTRIBUTES = ('source', 'resource_metadata') -class Sample(base.Resource): +class OldSample(base.Resource): + """Represents API v2 OldSample object. + + Model definition: + http://docs.openstack.org/developer/ceilometer/webapi/v2.html#OldSample + """ def __repr__(self): - return "" % self._info + return "" % self._info -class SampleManager(base.Manager): - resource_class = Sample +class OldSampleManager(base.Manager): + resource_class = OldSample @staticmethod def _path(counter_name=None): @@ -49,4 +54,29 @@ class SampleManager(base.Manager): url = self._path(counter_name=kwargs['counter_name']) body = self.api.post(url, json=[new]).json() if body: - return [Sample(self, b) for b in body] + return [OldSample(self, b) for b in body] + + +class Sample(base.Resource): + """Represents API v2 Sample object. + + Model definition: + http://docs.openstack.org/developer/ceilometer/webapi/v2.html#Sample + """ + def __repr__(self): + return "" % self._info + + +class SampleManager(base.Manager): + resource_class = Sample + + def list(self, q=None, limit=None): + params = ['limit=%s' % str(limit)] if limit else None + return self._list(options.build_url("/v2/samples", q, params)) + + def get(self, sample_id): + path = "/v2/samples/" + sample_id + try: + return self._list(path, expect_single=True)[0] + except IndexError: + return None diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 7cab85e..145d542 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -124,12 +124,19 @@ def do_statistics(cc, args): @utils.arg('-q', '--query', metavar='', help='key[op]data_type::value; list. data_type is optional, ' 'but if supplied must be string, integer, float, or boolean.') -@utils.arg('-m', '--meter', metavar='', required=True, +@utils.arg('-m', '--meter', metavar='', action=NotEmptyAction, help='Name of meter to show samples for.') @utils.arg('-l', '--limit', metavar='', help='Maximum number of samples to return.') def do_sample_list(cc, args): - """List the samples for a meter.""" + """List the samples (return OldSample objects if -m/--meter is set).""" + if not args.meter: + return _do_sample_list(cc, args) + else: + return _do_old_sample_list(cc, args) + + +def _do_old_sample_list(cc, args): fields = {'meter_name': args.meter, 'q': options.cli_to_array(args.query), 'limit': args.limit} @@ -142,8 +149,36 @@ def do_sample_list(cc, args): 'Timestamp'] fields = ['resource_id', 'counter_name', 'counter_type', 'counter_volume', 'counter_unit', 'timestamp'] - utils.print_list(samples, fields, field_labels, - sortby=None) + utils.print_list(samples, fields, field_labels, sortby=None) + + +def _do_sample_list(cc, args): + fields = { + 'q': options.cli_to_array(args.query), + 'limit': args.limit + } + samples = cc.new_samples.list(**fields) + field_labels = ['ID', 'Resource ID', 'Name', 'Type', 'Volume', 'Unit', + 'Timestamp'] + fields = ['id', 'resource_id', 'meter', 'type', 'volume', 'unit', + 'timestamp'] + utils.print_list(samples, fields, field_labels, sortby=None) + + +@utils.arg('sample_id', metavar='', action=NotEmptyAction, + help='ID (aka message ID) of the sample to show.') +def do_sample_show(cc, args): + '''Show an sample.''' + sample = cc.samples.get(args.sample_id) + + if sample is None: + raise exc.CommandError('Sample not found: %s' % args.sample_id) + + fields = ['id', 'meter', 'volume', 'type', 'unit', 'source', + 'resource_id', 'user_id', 'project_id', + 'timestamp', 'recorded_at', 'metadata'] + data = dict((f, getattr(sample, f, '')) for f in fields) + utils.print_dict(data, wrap=72) @utils.arg('--project-id', metavar='', @@ -181,7 +216,7 @@ def do_sample_create(cc, args={}): fields[k] = json.loads(v) else: fields[arg_to_field_mapping.get(k, k)] = v - sample = cc.samples.create(**fields) + sample = cc.old_samples.create(**fields) fields = ['counter_name', 'user_id', 'resource_id', 'timestamp', 'message_id', 'source', 'counter_unit', 'counter_volume', 'project_id', 'resource_metadata', diff --git a/requirements.txt b/requirements.txt index 99d597a..0fe0896 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ pbr>=0.6,!=0.7,<1.0 argparse iso8601>=0.1.9 oslo.i18n>=1.3.0 # Apache-2.0 +oslo.serialization>=1.2.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0 PrettyTable>=0.7,<0.8 python-keystoneclient>=0.11.1 -- cgit v1.2.1