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
This commit is contained in:
ZhiQiang Fan
2014-11-02 10:25:19 +08:00
committed by ZhiQiang Fan
parent 945f9a392a
commit 1831bc1160
7 changed files with 338 additions and 73 deletions

View File

@@ -19,6 +19,7 @@ import sys
import textwrap import textwrap
import uuid import uuid
from oslo.serialization import jsonutils
from oslo.utils import encodeutils from oslo.utils import encodeutils
from oslo.utils import importutils from oslo.utils import importutils
import prettytable import prettytable
@@ -89,7 +90,7 @@ def print_dict(d, dict_property="Property", wrap=0):
for k, v in sorted(six.iteritems(d)): for k, v in sorted(six.iteritems(d)):
# convert dict to str to check length # convert dict to str to check length
if isinstance(v, dict): if isinstance(v, dict):
v = str(v) v = jsonutils.dumps(v)
# if value has a newline, add in multiple rows # if value has a newline, add in multiple rows
# e.g. fault with stacktrace # e.g. fault with stacktrace
if v and isinstance(v, six.string_types) and r'\n' in v: if v and isinstance(v, six.string_types) and r'\n' in v:

View File

@@ -20,62 +20,103 @@ from ceilometerclient.openstack.common.apiclient import fake_client
from ceilometerclient.tests import utils from ceilometerclient.tests import utils
import ceilometerclient.v2.samples import ceilometerclient.v2.samples
GET_SAMPLE = {u'counter_name': u'instance', GET_OLD_SAMPLE = {u'counter_name': u'instance',
u'user_id': u'user-id', u'user_id': u'user-id',
u'resource_id': u'resource-id', u'resource_id': u'resource-id',
u'timestamp': u'2012-07-02T10:40:00', u'timestamp': u'2012-07-02T10:40:00',
u'source': u'test_source', u'source': u'test_source',
u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f', u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f',
u'counter_unit': u'', u'counter_unit': u'',
u'counter_volume': 1.0, u'counter_volume': 1.0,
u'project_id': u'project1', u'project_id': u'project1',
u'resource_metadata': {u'tag': u'self.counter', u'resource_metadata': {u'tag': u'self.counter',
u'display_name': u'test-server'}, u'display_name': u'test-server'},
u'counter_type': u'cumulative'} u'counter_type': u'cumulative'}
CREATE_SAMPLE = copy.deepcopy(GET_SAMPLE) CREATE_SAMPLE = copy.deepcopy(GET_OLD_SAMPLE)
del CREATE_SAMPLE['message_id'] del CREATE_SAMPLE['message_id']
del CREATE_SAMPLE['source'] del CREATE_SAMPLE['source']
base_url = '/v2/meters/instance' GET_SAMPLE = {
args = ('q.field=resource_id&q.field=source&q.op=&q.op=' "user_id": None,
'&q.type=&q.type=&q.value=foo&q.value=bar') "resource_id": "9b651dfd-7d30-402b-972e-212b2c4bfb05",
args_limit = 'limit=1' "timestamp": "2014-11-03T13:37:46",
fixtures = { "meter": "image",
base_url: "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': (
{}, {},
[GET_SAMPLE] [GET_OLD_SAMPLE]
), ),
'POST': ( 'POST': (
{}, {},
[CREATE_SAMPLE], [CREATE_SAMPLE],
), ),
}, },
'%s?%s' % (base_url, args): '%s?%s' % (METER_URL, QUERIES): {
{
'GET': ( 'GET': (
{}, {},
[], [],
), ),
}, },
'%s?%s' % (base_url, args_limit): '%s?%s' % (METER_URL, LIMIT): {
{
'GET': ( '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): def setUp(self):
super(SampleManagerTest, self).setUp() super(OldSampleManagerTest, self).setUp()
self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) self.http_client = fake_client.FakeHTTPClient(
fixtures=OLD_SAMPLE_FIXTURES)
self.api = client.BaseClient(self.http_client) 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): def test_list_by_meter_name(self):
samples = list(self.mgr.list(meter_name='instance')) samples = list(self.mgr.list(meter_name='instance'))
@@ -94,7 +135,7 @@ class SampleManagerTest(utils.BaseTestCase):
{"field": "source", {"field": "source",
"value": "bar"}, "value": "bar"},
])) ]))
expect = ['GET', '%s?%s' % (base_url, args)] expect = ['GET', '%s?%s' % (METER_URL, QUERIES)]
self.http_client.assert_called(*expect) self.http_client.assert_called(*expect)
self.assertEqual(len(samples), 0) self.assertEqual(len(samples), 0)
@@ -111,3 +152,47 @@ class SampleManagerTest(utils.BaseTestCase):
expect = ['GET', '/v2/meters/instance?limit=1'] expect = ['GET', '/v2/meters/instance?limit=1']
self.http_client.assert_called(*expect) self.http_client.assert_called(*expect)
self.assertEqual(len(samples), 1) 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())

View File

@@ -324,43 +324,70 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
class ShellSampleListCommandTest(utils.BaseTestCase): class ShellSampleListCommandTest(utils.BaseTestCase):
METER = 'cpu_util' METER = 'cpu_util'
SAMPLES = [{"counter_name": "cpu_util", SAMPLE_VALUES = (
"resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", ("cpu_util",
"timestamp": "2013-10-15T05:50:30", "5dcf5537-3161-4e25-9235-407e1385bd35",
"counter_unit": "%", "2013-10-15T05:50:30",
"counter_volume": 0.261666666667, "%",
"counter_type": "gauge"}, 0.261666666667,
{"counter_name": "cpu_util", "gauge",
"resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", "86536501-b2c9-48f6-9c6a-7a5b14ba7482"),
"timestamp": "2013-10-15T05:50:29", ("cpu_util",
"counter_unit": "%", "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f",
"counter_volume": 0.261666666667, "2013-10-15T05:50:29",
"counter_type": "gauge"}, "%",
{"counter_name": "cpu_util", 0.261666666667,
"resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", "gauge",
"timestamp": "2013-10-15T05:40:30", "fe2a91ec-602b-4b55-8cba-5302ce3b916e",),
"counter_unit": "%", ("cpu_util",
"counter_volume": 0.251247920133, "5dcf5537-3161-4e25-9235-407e1385bd35",
"counter_type": "gauge"}, "2013-10-15T05:40:30",
{"counter_name": "cpu_util", "%",
"resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", 0.251247920133,
"timestamp": "2013-10-15T05:40:29", "gauge",
"counter_unit": "%", "52768bcb-b4e9-4db9-a30c-738c758b6f43"),
"counter_volume": 0.26, ("cpu_util",
"counter_type": "gauge"}] "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): def setUp(self):
super(ShellSampleListCommandTest, self).setUp() super(ShellSampleListCommandTest, self).setUp()
self.cc = mock.Mock() self.cc = mock.Mock()
self.args = mock.Mock() self.args = mock.Mock()
self.args.meter = self.METER
self.args.query = None self.args.query = None
self.args.limit = None self.args.limit = None
@mock.patch('sys.stdout', new=six.StringIO()) @mock.patch('sys.stdout', new=six.StringIO())
def test_sample_list(self): def test_old_sample_list(self):
sample_list = [samples.Sample(mock.Mock(), sample) self.args.meter = self.METER
for sample in self.SAMPLES] sample_list = [samples.OldSample(mock.Mock(), sample)
for sample in self.OLD_SAMPLES]
self.cc.samples.list.return_value = sample_list self.cc.samples.list.return_value = sample_list
ceilometer_shell.do_sample_list(self.cc, self.args) ceilometer_shell.do_sample_list(self.cc, self.args)
@@ -388,6 +415,91 @@ class ShellSampleListCommandTest(utils.BaseTestCase):
+------+---------------------+ +------+---------------------+
''', sys.stdout.getvalue()) ''', 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): class ShellSampleCreateCommandTest(utils.BaseTestCase):
@@ -422,9 +534,9 @@ class ShellSampleCreateCommandTest(utils.BaseTestCase):
@mock.patch('sys.stdout', new=six.StringIO()) @mock.patch('sys.stdout', new=six.StringIO())
def test_sample_create(self): def test_sample_create(self):
ret_sample = [samples.Sample(mock.Mock(), sample) ret_sample = [samples.OldSample(mock.Mock(), sample)
for sample in self.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) ceilometer_shell.do_sample_create(self.cc, self.args)

View File

@@ -63,7 +63,8 @@ class Client(object):
self.http_client = client.BaseClient(self.client) self.http_client = client.BaseClient(self.client)
self.meters = meters.MeterManager(self.http_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.statistics = statistics.StatisticsManager(self.http_client)
self.resources = resources.ResourceManager(self.http_client) self.resources = resources.ResourceManager(self.http_client)
self.alarms = alarms.AlarmManager(self.http_client) self.alarms = alarms.AlarmManager(self.http_client)

View File

@@ -26,13 +26,18 @@ CREATION_ATTRIBUTES = ('source',
'resource_metadata') '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): def __repr__(self):
return "<Sample %s>" % self._info return "<OldSample %s>" % self._info
class SampleManager(base.Manager): class OldSampleManager(base.Manager):
resource_class = Sample resource_class = OldSample
@staticmethod @staticmethod
def _path(counter_name=None): def _path(counter_name=None):
@@ -49,4 +54,29 @@ class SampleManager(base.Manager):
url = self._path(counter_name=kwargs['counter_name']) url = self._path(counter_name=kwargs['counter_name'])
body = self.api.post(url, json=[new]).json() body = self.api.post(url, json=[new]).json()
if body: 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 "<Sample %s>" % 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

View File

@@ -124,12 +124,19 @@ def do_statistics(cc, args):
@utils.arg('-q', '--query', metavar='<QUERY>', @utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, ' help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.') 'but if supplied must be string, integer, float, or boolean.')
@utils.arg('-m', '--meter', metavar='<NAME>', required=True, @utils.arg('-m', '--meter', metavar='<NAME>',
action=NotEmptyAction, help='Name of meter to show samples for.') action=NotEmptyAction, help='Name of meter to show samples for.')
@utils.arg('-l', '--limit', metavar='<NUMBER>', @utils.arg('-l', '--limit', metavar='<NUMBER>',
help='Maximum number of samples to return.') help='Maximum number of samples to return.')
def do_sample_list(cc, args): 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, fields = {'meter_name': args.meter,
'q': options.cli_to_array(args.query), 'q': options.cli_to_array(args.query),
'limit': args.limit} 'limit': args.limit}
@@ -142,8 +149,36 @@ def do_sample_list(cc, args):
'Timestamp'] 'Timestamp']
fields = ['resource_id', 'counter_name', 'counter_type', fields = ['resource_id', 'counter_name', 'counter_type',
'counter_volume', 'counter_unit', 'timestamp'] 'counter_volume', 'counter_unit', 'timestamp']
utils.print_list(samples, fields, field_labels, utils.print_list(samples, fields, field_labels, sortby=None)
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='<SAMPLE_ID>', 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='<PROJECT_ID>', @utils.arg('--project-id', metavar='<PROJECT_ID>',
@@ -181,7 +216,7 @@ def do_sample_create(cc, args={}):
fields[k] = json.loads(v) fields[k] = json.loads(v)
else: else:
fields[arg_to_field_mapping.get(k, k)] = v 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', fields = ['counter_name', 'user_id', 'resource_id',
'timestamp', 'message_id', 'source', 'counter_unit', 'timestamp', 'message_id', 'source', 'counter_unit',
'counter_volume', 'project_id', 'resource_metadata', 'counter_volume', 'project_id', 'resource_metadata',

View File

@@ -5,6 +5,7 @@ pbr>=0.6,!=0.7,<1.0
argparse argparse
iso8601>=0.1.9 iso8601>=0.1.9
oslo.i18n>=1.3.0 # Apache-2.0 oslo.i18n>=1.3.0 # Apache-2.0
oslo.serialization>=1.2.0 # Apache-2.0
oslo.utils>=1.2.0 # Apache-2.0 oslo.utils>=1.2.0 # Apache-2.0
PrettyTable>=0.7,<0.8 PrettyTable>=0.7,<0.8
python-keystoneclient>=0.11.1 python-keystoneclient>=0.11.1