From c74231e74473cdfc8037731b6e9beb539d491814 Mon Sep 17 00:00:00 2001 From: John Herndon Date: Wed, 30 Oct 2013 10:44:53 -0600 Subject: [PATCH] Support the Event API This patch adds support for the currently available Event API, including events, event types, and traits. Add an optional data type to the query, ex: ceilometer list-events -q 'hostname=string::localhost" bp extend-client-operations Change-Id: Icea9bd67f8ee4ff2bf9da9ff6894218689580eb3 --- ceilometerclient/common/utils.py | 15 ++ ceilometerclient/tests/v2/test_alarms.py | 8 +- ceilometerclient/tests/v2/test_event_types.py | 47 +++++ ceilometerclient/tests/v2/test_events.py | 189 ++++++++++++++++++ ceilometerclient/tests/v2/test_options.py | 88 +++++++- ceilometerclient/tests/v2/test_resources.py | 5 +- ceilometerclient/tests/v2/test_samples.py | 3 +- ceilometerclient/tests/v2/test_shell.py | 3 +- ceilometerclient/tests/v2/test_statistics.py | 3 +- .../tests/v2/test_trait_descriptions.py | 55 +++++ ceilometerclient/tests/v2/test_traits.py | 61 ++++++ ceilometerclient/v2/client.py | 8 + ceilometerclient/v2/event_types.py | 31 +++ ceilometerclient/v2/events.py | 37 ++++ ceilometerclient/v2/options.py | 30 ++- ceilometerclient/v2/shell.py | 86 ++++++-- ceilometerclient/v2/trait_descriptions.py | 29 +++ ceilometerclient/v2/traits.py | 29 +++ 18 files changed, 692 insertions(+), 35 deletions(-) create mode 100644 ceilometerclient/tests/v2/test_event_types.py create mode 100644 ceilometerclient/tests/v2/test_events.py create mode 100644 ceilometerclient/tests/v2/test_trait_descriptions.py create mode 100644 ceilometerclient/tests/v2/test_traits.py create mode 100644 ceilometerclient/v2/event_types.py create mode 100644 ceilometerclient/v2/events.py create mode 100644 ceilometerclient/v2/trait_descriptions.py create mode 100644 ceilometerclient/v2/traits.py diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index ffeb1bf1..4f532e5e 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -66,6 +66,21 @@ def print_list(objs, fields, field_labels, formatters={}, sortby=0): sortby_index=sortby) +def nested_dict_formatter(field): + # (TMaddox) Because the formatting scheme actually drops the whole object + # into the formatter, rather than just the specified field, we have to + # extract it and then pass the value. + return lambda o: format_nested_dict(getattr(o, field)) + + +def format_nested_dict(d): + pt = prettytable.PrettyTable(caching=False, print_empty=False, + header=False, hrules=prettytable.FRAME) + for k, v in six.iteritems(d): + pt.add_row([k, format_nested_dict(v) if isinstance(v, dict) else v]) + return pt.get_string() + + def print_dict(d, dict_property="Property", wrap=0): pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False, print_empty=False) diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index 3f8a2760..6665ef76 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -189,7 +189,7 @@ fixtures = { }, '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' - '&q.value=project-id&q.value=SwiftObjectAlarm': + '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm': { 'GET': ( {}, @@ -210,7 +210,7 @@ fixtures = { ALARM_HISTORY, ), }, - '/v2/alarms/alarm-id/history?q.field=timestamp&q.op=&q.value=NOW': + '/v2/alarms/alarm-id/history?q.field=timestamp&q.op=&q.type=&q.value=NOW': { 'GET': ( {}, @@ -244,7 +244,7 @@ class AlarmManagerTest(testtools.TestCase): expect = [ ('GET', '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' - '&q.value=project-id&q.value=SwiftObjectAlarm', + '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm', {}, None), ] self.assertEqual(self.api.calls, expect) @@ -334,7 +334,7 @@ class AlarmManagerTest(testtools.TestCase): def test_get_constrained_history(self): q = [dict(field='timestamp', value='NOW')] url = ('/v2/alarms/alarm-id/history?q.field=timestamp' - '&q.op=&q.value=NOW') + '&q.op=&q.type=&q.value=NOW') self._do_test_get_history(q, url) diff --git a/ceilometerclient/tests/v2/test_event_types.py b/ceilometerclient/tests/v2/test_event_types.py new file mode 100644 index 00000000..85c59ec1 --- /dev/null +++ b/ceilometerclient/tests/v2/test_event_types.py @@ -0,0 +1,47 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.tests import utils +import ceilometerclient.v2.event_types + + +fixtures = { + '/v2/event_types/': { + 'GET': ( + {}, + ['Foo', 'Bar', 'Sna', 'Fu'] + ), + } +} + + +class EventTypesManagerTest(utils.BaseTestCase): + + def setUp(self): + super(EventTypesManagerTest, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.mgr = ceilometerclient.v2.event_types.EventTypeManager(self.api) + + def test_list(self): + event_types = list(self.mgr.list()) + expect = [ + ('GET', '/v2/event_types/', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(event_types), 4) + self.assertEqual(event_types[0].event_type, "Foo") + self.assertEqual(event_types[1].event_type, "Bar") + self.assertEqual(event_types[2].event_type, "Sna") + self.assertEqual(event_types[3].event_type, "Fu") diff --git a/ceilometerclient/tests/v2/test_events.py b/ceilometerclient/tests/v2/test_events.py new file mode 100644 index 00000000..ee90a14e --- /dev/null +++ b/ceilometerclient/tests/v2/test_events.py @@ -0,0 +1,189 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.tests import utils +import ceilometerclient.v2.events + + +fixtures = { + '/v2/events': { + 'GET': ( + {}, + [ + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'abc', + 'message_id': '1'}, + }, + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'def', + 'message_id': '2'}, + }, + { + 'event_type': 'Bar', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_B': 'bartrait', + 'message_id': '3'}, + }, + ] + ), + }, + '/v2/events?q.field=hostname&q.op=&q.type=string&q.value=localhost': + { + 'GET': ( + {}, + [ + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'abc', + 'hostname': 'localhost', + 'message_id': '1'}, + }, + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'def', + 'hostname': 'localhost', + 'message_id': '2'}, + } + ] + ), + }, + '/v2/events?q.field=hostname&q.op=&q.type=&q.value=foreignhost': + { + 'GET': ( + {}, + [ + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'abc', + 'hostname': 'foreignhost', + 'message_id': '1'}, + }, + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'def', + 'hostname': 'foreignhost', + 'message_id': '2'}, + } + ] + ), + }, + '/v2/events?q.field=hostname&q.field=num_cpus&q.op=&q.op=' + '&q.type=&q.type=integer&q.value=localhost&q.value=5': + { + 'GET': ( + {}, + [ + { + 'event_type': 'Bar', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'abc', + 'hostname': 'localhost', + 'num_cpus': '5', + 'message_id': '1'}, + }, + ] + ), + }, + + '/v2/events/2': + { + 'GET': ( + {}, + { + 'event_type': 'Foo', + 'generated': '1970-01-01T00:00:00', + 'traits': {'trait_A': 'def', + 'message_id': '2', + 'intTrait': '42'}, + } + ), + }, +} + + +class EventManagerTest(utils.BaseTestCase): + + def setUp(self): + super(EventManagerTest, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.mgr = ceilometerclient.v2.events.EventManager(self.api) + + def test_list_all(self): + events = list(self.mgr.list()) + expect = [ + ('GET', '/v2/events', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(events), 3) + self.assertEqual(events[0].event_type, 'Foo') + self.assertEqual(events[1].event_type, 'Foo') + self.assertEqual(events[2].event_type, 'Bar') + + def test_list_one(self): + event = self.mgr.get(2) + expect = [ + ('GET', '/v2/events/2', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertTrue(event) + self.assertEqual(event.event_type, 'Foo') + + def test_list_with_query(self): + events = list(self.mgr.list(q=[{"field": "hostname", + "value": "localhost", + "type": "string"}])) + expect = [ + ('GET', '/v2/events?q.field=hostname&q.op=&q.type=string' + '&q.value=localhost', + {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(events), 2) + self.assertEqual(events[0].event_type, 'Foo') + + def test_list_with_query_no_type(self): + events = list(self.mgr.list(q=[{"field": "hostname", + "value": "foreignhost"}])) + expect = [ + ('GET', '/v2/events?q.field=hostname&q.op=' + '&q.type=&q.value=foreignhost', + {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(events), 2) + self.assertEqual(events[0].event_type, 'Foo') + + def test_list_with_multiple_filters(self): + events = list(self.mgr.list(q=[{"field": "hostname", + "value": "localhost"}, + {"field": "num_cpus", + "value": "5", + "type": "integer"}])) + + expect = [ + ('GET', '/v2/events?q.field=hostname&q.field=num_cpus&q.op=&q.op=' + '&q.type=&q.type=integer&q.value=localhost&q.value=5', + {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(events), 1) diff --git a/ceilometerclient/tests/v2/test_options.py b/ceilometerclient/tests/v2/test_options.py index b771ec6b..8cbf73ab 100644 --- a/ceilometerclient/tests/v2/test_options.py +++ b/ceilometerclient/tests/v2/test_options.py @@ -21,7 +21,7 @@ class BuildUrlTest(utils.BaseTestCase): url = options.build_url('/', [{'field': 'this', 'op': 'gt', 'value': 43}]) - self.assertEqual(url, '/?q.field=this&q.op=gt&q.value=43') + self.assertEqual(url, '/?q.field=this&q.op=gt&q.type=&q.value=43') def test_two(self): url = options.build_url('/', [{'field': 'this', @@ -32,13 +32,14 @@ class BuildUrlTest(utils.BaseTestCase): 'value': 88}]) ops = 'q.op=gt&q.op=lt' vals = 'q.value=43&q.value=88' + types = 'q.type=&q.type=' fields = 'q.field=this&q.field=that' - self.assertEqual(url, '/?%s&%s&%s' % (fields, ops, vals)) + self.assertEqual(url, '/?%s&%s&%s&%s' % (fields, ops, types, vals)) def test_default_op(self): url = options.build_url('/', [{'field': 'this', 'value': 43}]) - self.assertEqual(url, '/?q.field=this&q.op=&q.value=43') + self.assertEqual(url, '/?q.field=this&q.op=&q.type=&q.value=43') def test_one_param(self): url = options.build_url('/', None, ['period=60']) @@ -49,26 +50,38 @@ class BuildUrlTest(utils.BaseTestCase): 'others=value']) self.assertEqual(url, '/?period=60&others=value') + def test_with_data_type(self): + url = options.build_url('/', [{'field': 'f1', + 'value': '10', + 'type': 'integer'}]) + + self.assertEqual('/?q.field=f1&q.op=&q.type=integer&q.value=10', url) + class CliTest(utils.BaseTestCase): def test_one(self): ar = options.cli_to_array('this<=34') - self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'}]) + self.assertEqual(ar, [{'field': 'this', 'op': 'le', + 'value': '34', 'type': ''}]) def test_two(self): ar = options.cli_to_array('this<=34;that!=foo') - self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'}, - {'field': 'that', 'op': 'ne', 'value': 'foo'}]) + self.assertEqual(ar, [{'field': 'this', 'op': 'le', + 'value': '34', 'type': ''}, + {'field': 'that', 'op': 'ne', + 'value': 'foo', 'type': ''}]) def test_negative(self): ar = options.cli_to_array('this>=-783') - self.assertEqual(ar, [{'field': 'this', 'op': 'ge', 'value': '-783'}]) + self.assertEqual(ar, [{'field': 'this', 'op': 'ge', + 'value': '-783', 'type': ''}]) def test_float(self): ar = options.cli_to_array('this<=283.347') self.assertEqual(ar, [{'field': 'this', - 'op': 'le', 'value': '283.347'}]) + 'op': 'le', 'value': '283.347', + 'type': ''}]) def test_invalid_seperator(self): self.assertRaises(ValueError, options.cli_to_array, @@ -81,4 +94,61 @@ class CliTest(utils.BaseTestCase): def test_with_dot(self): ar = options.cli_to_array('metadata.this<=34') self.assertEqual(ar, [{'field': 'metadata.this', - 'op': 'le', 'value': '34'}]) + 'op': 'le', 'value': '34', + 'type': ''}]) + + def test_without_data_type(self): + ar = options.cli_to_array('hostname=localhost') + self.assertEqual(ar, [{'field': 'hostname', + 'op': 'eq', + 'value': 'localhost', + 'type': ''}]) + + def test_with_string_data_type(self): + ar = options.cli_to_array('hostname=string::localhost') + self.assertEqual(ar, [{'field': 'hostname', + 'op': 'eq', + 'type': 'string', + 'value': 'localhost'}]) + + def test_with_int_data_type(self): + ar = options.cli_to_array('port=integer::1234') + self.assertEqual(ar, [{'field': 'port', + 'op': 'eq', + 'type': 'integer', + 'value': '1234'}]) + + def test_with_bool_data_type(self): + ar = options.cli_to_array('port=boolean::true') + self.assertEqual(ar, [{'field': 'port', + 'op': 'eq', + 'type': 'boolean', + 'value': 'true'}]) + + def test_with_float_data_type(self): + ar = options.cli_to_array('average=float::1234.5678') + self.assertEqual(ar, [{'field': 'average', + 'op': 'eq', + 'type': 'float', + 'value': '1234.5678'}]) + + def test_with_datetime_data_type(self): + ar = options.cli_to_array('timestamp=datetime::sometimestamp') + self.assertEqual(ar, [{'field': 'timestamp', + 'op': 'eq', + 'type': 'datetime', + 'value': 'sometimestamp'}]) + + def test_with_incorrect_type(self): + ar = options.cli_to_array('timestamp=invalid::sometimestamp') + self.assertEqual(ar, [{'field': 'timestamp', + 'op': 'eq', + 'type': '', + 'value': 'invalid::sometimestamp'}]) + + def test_with_single_colon(self): + ar = options.cli_to_array('timestamp=datetime:sometimestamp') + self.assertEqual(ar, [{'field': 'timestamp', + 'op': 'eq', + 'type': '', + 'value': 'datetime:sometimestamp'}]) diff --git a/ceilometerclient/tests/v2/test_resources.py b/ceilometerclient/tests/v2/test_resources.py index 0ddccb07..13a29fec 100644 --- a/ceilometerclient/tests/v2/test_resources.py +++ b/ceilometerclient/tests/v2/test_resources.py @@ -37,7 +37,7 @@ fixtures = { ] ), }, - '/v2/resources?q.field=resource_id&q.op=&q.value=a': + '/v2/resources?q.field=resource_id&q.op=&q.type=&q.value=a': { 'GET': ( {}, @@ -97,7 +97,8 @@ class ResourceManagerTest(utils.BaseTestCase): "value": "a"}, ])) expect = [ - ('GET', '/v2/resources?q.field=resource_id&q.op=&q.value=a', + ('GET', '/v2/resources?q.field=resource_id&q.op=' + '&q.type=&q.value=a', {}, None), ] self.assertEqual(self.api.calls, expect) diff --git a/ceilometerclient/tests/v2/test_samples.py b/ceilometerclient/tests/v2/test_samples.py index c279f35a..7b0b8787 100644 --- a/ceilometerclient/tests/v2/test_samples.py +++ b/ceilometerclient/tests/v2/test_samples.py @@ -35,7 +35,8 @@ 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.value=foo&q.value=bar' +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: diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 3b24b296..99a43025 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -133,7 +133,8 @@ class ShellAlarmHistoryCommandTest(utils.BaseTestCase): def test_alarm_constrained_history(self): parsed_query = [dict(field='timestamp', value='2013-10-03T08:59:28', - op='gt')] + op='gt', + type='')] self._do_test_alarm_history(raw_query='timestamp>2013-10-03T08:59:28', parsed_query=parsed_query) diff --git a/ceilometerclient/tests/v2/test_statistics.py b/ceilometerclient/tests/v2/test_statistics.py index e61afb82..7d6bf7ec 100644 --- a/ceilometerclient/tests/v2/test_statistics.py +++ b/ceilometerclient/tests/v2/test_statistics.py @@ -17,7 +17,8 @@ from ceilometerclient.tests import utils import ceilometerclient.v2.statistics base_url = '/v2/meters/instance/statistics' -qry = 'q.field=resource_id&q.field=source&q.op=&q.op=&q.value=foo&q.value=bar' +qry = ('q.field=resource_id&q.field=source&q.op=&q.op=' + '&q.type=&q.type=&q.value=foo&q.value=bar') period = '&period=60' samples = [ {u'count': 135, diff --git a/ceilometerclient/tests/v2/test_trait_descriptions.py b/ceilometerclient/tests/v2/test_trait_descriptions.py new file mode 100644 index 00000000..4c84f3c3 --- /dev/null +++ b/ceilometerclient/tests/v2/test_trait_descriptions.py @@ -0,0 +1,55 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.tests import utils +import ceilometerclient.v2.trait_descriptions + + +fixtures = { + '/v2/event_types/Foo/traits': { + 'GET': ( + {}, + [ + {'name': 'trait_1', 'type': 'string'}, + {'name': 'trait_2', 'type': 'integer'}, + {'name': 'trait_3', 'type': 'datetime'} + ] + ), + } +} + + +class TraitDescriptionManagerTest(utils.BaseTestCase): + + def setUp(self): + super(TraitDescriptionManagerTest, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.mgr = (ceilometerclient.v2.trait_descriptions. + TraitDescriptionManager(self.api)) + + def test_list(self): + trait_descriptions = list(self.mgr.list('Foo')) + expect = [ + ('GET', '/v2/event_types/Foo/traits', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(trait_descriptions), 3) + for i, vals in enumerate([('trait_1', 'string'), + ('trait_2', 'integer'), + ('trait_3', 'datetime')]): + + name, type = vals + self.assertEqual(trait_descriptions[i].name, name) + self.assertEqual(trait_descriptions[i].type, type) diff --git a/ceilometerclient/tests/v2/test_traits.py b/ceilometerclient/tests/v2/test_traits.py new file mode 100644 index 00000000..697acf10 --- /dev/null +++ b/ceilometerclient/tests/v2/test_traits.py @@ -0,0 +1,61 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.tests import utils +import ceilometerclient.v2.traits + + +fixtures = { + '/v2/event_types/Foo/traits/trait_1': { + 'GET': ( + {}, + [ + {'name': 'trait_1', + 'type': 'datetime', + 'value': '2014-01-07T17:22:10.925553'}, + {'name': 'trait_1', + 'type': 'datetime', + 'value': '2014-01-07T17:23:10.925553'} + ] + ), + } +} + + +class TraitManagerTest(utils.BaseTestCase): + + def setUp(self): + super(TraitManagerTest, self).setUp() + self.api = utils.FakeAPI(fixtures) + self.mgr = ceilometerclient.v2.traits.TraitManager(self.api) + + def test_list(self): + traits = list(self.mgr.list('Foo', 'trait_1')) + expect = [ + ('GET', '/v2/event_types/Foo/traits/trait_1', {}, None), + ] + self.assertEqual(self.api.calls, expect) + self.assertEqual(len(traits), 2) + for i, vals in enumerate([('trait_1', + 'datetime', + '2014-01-07T17:22:10.925553'), + ('trait_1', + 'datetime', + '2014-01-07T17:23:10.925553')]): + + name, type, value = vals + self.assertEqual(traits[i].name, name) + self.assertEqual(traits[i].type, type) + self.assertEqual(traits[i].value, value) diff --git a/ceilometerclient/v2/client.py b/ceilometerclient/v2/client.py index e527d854..e11049f8 100644 --- a/ceilometerclient/v2/client.py +++ b/ceilometerclient/v2/client.py @@ -15,10 +15,14 @@ from ceilometerclient.common import http from ceilometerclient.v2 import alarms +from ceilometerclient.v2 import event_types +from ceilometerclient.v2 import events from ceilometerclient.v2 import meters from ceilometerclient.v2 import resources from ceilometerclient.v2 import samples from ceilometerclient.v2 import statistics +from ceilometerclient.v2 import trait_descriptions +from ceilometerclient.v2 import traits class Client(http.HTTPClient): @@ -39,3 +43,7 @@ class Client(http.HTTPClient): self.statistics = statistics.StatisticsManager(self) self.resources = resources.ResourceManager(self) self.alarms = alarms.AlarmManager(self) + self.events = events.EventManager(self) + self.event_types = event_types.EventTypeManager(self) + self.traits = traits.TraitManager(self) + self.trait_info = trait_descriptions.TraitDescriptionManager(self) diff --git a/ceilometerclient/v2/event_types.py b/ceilometerclient/v2/event_types.py new file mode 100644 index 00000000..036eb3e5 --- /dev/null +++ b/ceilometerclient/v2/event_types.py @@ -0,0 +1,31 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.common import base + + +class EventType(base.Resource): + def __repr__(self): + return "" % self._info + + +def object_class_str(mgr, value, loaded): + return EventType(mgr, {"event_type": value}, loaded) + + +class EventTypeManager(base.Manager): + + def list(self): + return self._list('/v2/event_types/', obj_class=object_class_str) diff --git a/ceilometerclient/v2/events.py b/ceilometerclient/v2/events.py new file mode 100644 index 00000000..0e245bb1 --- /dev/null +++ b/ceilometerclient/v2/events.py @@ -0,0 +1,37 @@ +# -*- encoding: utf-8 -*- +# Copyright 2014 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.common import base +from ceilometerclient.v2 import options + + +class Event(base.Resource): + def __repr__(self): + return "" % self._info + + +class EventManager(base.Manager): + resource_class = Event + + def list(self, q=None): + path = '/v2/events' + return self._list(options.build_url(path, q)) + + def get(self, message_id): + path = '/v2/events/%s' + try: + return self._list(path % message_id, expect_single=True)[0] + except IndexError: + return None diff --git a/ceilometerclient/v2/options.py b/ceilometerclient/v2/options.py index 349e04ca..932ede13 100644 --- a/ceilometerclient/v2/options.py +++ b/ceilometerclient/v2/options.py @@ -28,10 +28,11 @@ def build_url(path, q, params=None): if q: query_params = {'q.field': [], 'q.value': [], - 'q.op': []} + 'q.op': [], + 'q.type': []} for query in q: - for name in ['field', 'op', 'value']: + for name in ['field', 'op', 'value', 'type']: query_params['q.%s' % name].append(query.get(name, '')) # Transform the dict to a sequence of two-element tuples in fixed @@ -50,13 +51,15 @@ def build_url(path, q, params=None): def cli_to_array(cli_query): - '''This converts from the cli list of queries to what is required + """This converts from the cli list of queries to what is required by the python api. so from: "this<=34;that=foo" to "[{field=this,op=le,value=34},{field=that,op=eq,value=foo}]" - ''' + + """ + if cli_query is None: return None @@ -77,6 +80,14 @@ def cli_to_array(cli_query): string) return frags + def split_by_data_type(string): + frags = re.findall(r'^(string|integer|float|datetime|boolean)(::)' + r'([^ -,\t\n\r\f\v]+)$', string) + + # frags[1] is the separator. Return a list without it if the type + # identifier was found. + return [frags[0][0], frags[0][2]] if frags else None + opts = [] queries = cli_query.split(';') for q in queries: @@ -90,6 +101,15 @@ def cli_to_array(cli_query): opt = {} opt['field'] = query[0] opt['op'] = op_lookup[query[1]] - opt['value'] = query[2] + + # Allow the data type of the value to be specified via ::, + # where type can be one of integer, string, float, datetime, boolean + value_frags = split_by_data_type(query[2]) + if not value_frags: + opt['value'] = query[2] + opt['type'] = '' + else: + opt['type'] = value_frags[0] + opt['value'] = value_frags[1] opts.append(opt) return opts diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index 598d3042..6b63873f 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -35,7 +35,8 @@ OPERATORS_STRING = dict(gt='>', ge='>=', @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + 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, help='Name of meter to show samples for.') @utils.arg('-p', '--period', metavar='', @@ -60,7 +61,8 @@ def do_statistics(cc, args): @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + 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, help='Name of meter to show samples for.') @utils.arg('-l', '--limit', metavar='', @@ -121,7 +123,8 @@ def do_sample_create(cc, args={}): @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean') def do_meter_list(cc, args={}): '''List the user's meters.''' meters = cc.meters.list(q=options.cli_to_array(args.query)) @@ -193,7 +196,8 @@ def alarm_change_detail_formatter(change): @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean') def do_alarm_list(cc, args={}): '''List the user's alarms.''' alarms = cc.alarms.list(q=options.cli_to_array(args.query)) @@ -326,9 +330,8 @@ def do_alarm_create(cc, args={}): dest='threshold_rule/threshold', help='Threshold to evaluate against') @utils.arg('-q', '--query', metavar='', - dest='threshold_rule/query', - help='The query to find the data for computing statistics ' - '(key[op]value; list.)') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean') @utils.arg('--repeat-actions', dest='repeat_actions', metavar='{True|False}', type=utils.string_to_bool, default=False, @@ -425,9 +428,8 @@ def do_alarm_update(cc, args={}): dest='threshold_rule/threshold', help='Threshold to evaluate against') @utils.arg('-q', '--query', metavar='', - dest='threshold_rule/query', - help='The query to find the data for computing statistics ' - '(key[op]value; list.)') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean') @utils.arg('--repeat-actions', dest='repeat_actions', metavar='{True|False}', type=utils.string_to_bool, help=('True if actions should be repeatedly notified ' @@ -512,7 +514,8 @@ def do_alarm_state_get(cc, args={}): @utils.arg('-a', '--alarm_id', metavar='', required=True, help='ID of the alarm for which history is shown.') @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean') def do_alarm_history(cc, args={}): '''Display the change history of an alarm.''' kwargs = dict(alarm_id=args.alarm_id, @@ -529,7 +532,8 @@ def do_alarm_history(cc, args={}): @utils.arg('-q', '--query', metavar='', - help='key[op]value; list.') + help='key[op]data_type::value; list. data_type is optional, ' + 'but if supplied must be string, integer, float, or boolean.') def do_resource_list(cc, args={}): '''List the resources.''' resources = cc.resources.list(q=options.cli_to_array(args.query)) @@ -553,3 +557,61 @@ def do_resource_show(cc, args={}): 'project_id', 'metadata'] data = dict([(f, getattr(resource, f, '')) for f in fields]) utils.print_dict(data, wrap=72) + + +@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 datetime.') +def do_event_list(cc, args={}): + '''List events.''' + events = cc.events.list(q=options.cli_to_array(args.query)) + field_labels = ['Message ID', 'Event Type', 'Generated', 'Traits'] + fields = ['message_id', 'event_type', 'generated', 'traits'] + utils.print_list(events, fields, field_labels, + formatters={ + 'traits': utils.nested_dict_formatter('traits')}) + + +@utils.arg('-m', '--message_id', metavar='', + help='The id of the event. Should be a UUID', + required=True) +def do_event_show(cc, args={}): + '''Show a particular event.''' + event = cc.events.get(args.message_id) + fields = ['event_type', 'generated', 'traits'] + data = dict([(f, getattr(event, f, '')) for f in fields]) + utils.print_dict(data, wrap=72) + + +def do_event_type_list(cc, args={}): + '''List event types.''' + event_types = cc.event_types.list() + utils.print_list(event_types, ['event_type'], ['Event Type']) + + +@utils.arg('-e', '--event_type', metavar='', + help='Type of the event for which traits will be shown', + required=True) +def do_trait_description_list(cc, args={}): + '''List trait info for an event type.''' + trait_descriptions = cc.trait_descriptions.list(args.event_type) + field_labels = ['Trait Name', 'Data Type'] + fields = ['name', 'type'] + utils.print_list(trait_descriptions, fields, field_labels) + + +@utils.arg('-e', '--event_type', metavar='', + help='Type of the event for which traits will listed', + required=True) +@utils.arg('-t', '--trait_name', metavar='', + help='The name of the trait to list', + required=True) +def do_trait_list(cc, args={}): + '''List trait all traits with name for Event Type + . + ''' + traits = cc.traits.list(args.event_type, args.trait_name) + field_labels = ['Trait Name', 'Value', 'Data Type'] + fields = ['name', 'value', 'type'] + utils.print_list(traits, fields, field_labels) diff --git a/ceilometerclient/v2/trait_descriptions.py b/ceilometerclient/v2/trait_descriptions.py new file mode 100644 index 00000000..b6a03729 --- /dev/null +++ b/ceilometerclient/v2/trait_descriptions.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.common import base + + +class TraitDescription(base.Resource): + def __repr__(self): + return "" % self._info + + +class TraitDescriptionManager(base.Manager): + resource_class = TraitDescription + + def list(self, event_type): + path = '/v2/event_types/%s/traits' % event_type + return self._list(path) diff --git a/ceilometerclient/v2/traits.py b/ceilometerclient/v2/traits.py new file mode 100644 index 00000000..fbeb22a5 --- /dev/null +++ b/ceilometerclient/v2/traits.py @@ -0,0 +1,29 @@ +# -*- encoding: utf-8 -*- +# Copyright 2013 Hewlett-Packard Development Company, L.P. +# +# 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 ceilometerclient.common import base + + +class Trait(base.Resource): + def __repr__(self): + return "" % self._info + + +class TraitManager(base.Manager): + resource_class = Trait + + def list(self, event_type, trait_name): + path = '/v2/event_types/%s/traits/%s' % (event_type, trait_name) + return self._list(path)