From b67d2c2dfb61dc116e6245d62bd454a972bbb51d Mon Sep 17 00:00:00 2001 From: Angus Salkeld Date: Mon, 3 Dec 2012 14:16:57 +1100 Subject: [PATCH] Implement /meters to make discovery "nicer" from the client The point of this api is to make discovery (esp. from a casual user) easier. So you don't really want to dump all the raw samples out just to see what is there. So instead "ceilometer meter-list" will GET /v1/meters (or /{proj|user|source}/{id}/meters) and this will just return a description (name, type, resource, user, etc) of the available meters, not each sample point. After this you will probably go and look at the samples that you are actually interested in. It is a kind of dynamic version of doc/source/measurements.rst Change-Id: I58f2757874ab151632b6d87043d6327104c5b65c --- ceilometer/api/v1.py | 60 +++++++++- ceilometer/storage/base.py | 11 ++ ceilometer/storage/impl_log.py | 16 +++ ceilometer/storage/impl_mongodb.py | 35 +++++- ceilometer/storage/impl_sqlalchemy.py | 44 +++++++- doc/source/measurements.rst | 12 ++ tests/api/v1/test_list_meters.py | 155 ++++++++++++++++++++++++++ tests/storage/test_impl_mongodb.py | 12 ++ tests/storage/test_impl_sqlalchemy.py | 14 +++ 9 files changed, 353 insertions(+), 6 deletions(-) create mode 100644 tests/api/v1/test_list_meters.py diff --git a/ceilometer/api/v1.py b/ceilometer/api/v1.py index 29fd0de2..999b86c6 100644 --- a/ceilometer/api/v1.py +++ b/ceilometer/api/v1.py @@ -40,10 +40,11 @@ # # [ ] /resources/ -- metadata # -# [ ] /projects//meters -- list of meters reporting for parent obj -# [ ] /resources//meters -- list of meters reporting for parent obj -# [ ] /sources//meters -- list of meters reporting for parent obj -# [ ] /users//meters -- list of meters reporting for parent obj +# [x] /meters -- list of meters +# [x] /projects//meters -- list of meters reporting for parent obj +# [x] /resources//meters -- list of meters reporting for parent obj +# [x] /sources//meters -- list of meters reporting for parent obj +# [x] /users//meters -- list of meters reporting for parent obj # # [x] /projects//meters/ -- events # [x] /resources//meters/ -- events @@ -99,6 +100,57 @@ def request_wants_html(): flask.request.accept_mimetypes['application/json'] +## APIs for working with meters. + + +@blueprint.route('/meters') +def list_meters_all(): + """Return a list of meters. + """ + meters = flask.request.storage_conn.get_meters() + return flask.jsonify(meters=list(meters)) + + +@blueprint.route('/resources//meters') +def list_meters_by_resource(resource): + """Return a list of meters by resource. + + :param resource: The ID of the resource. + """ + meters = flask.request.storage_conn.get_meters(resource=resource) + return flask.jsonify(meters=list(meters)) + + +@blueprint.route('/users//meters') +def list_meters_by_user(user): + """Return a list of meters by user. + + :param user: The ID of the owning user. + """ + meters = flask.request.storage_conn.get_meters(user=user) + return flask.jsonify(meters=list(meters)) + + +@blueprint.route('/projects//meters') +def list_meters_by_project(project): + """Return a list of meters by project. + + :param project: The ID of the owning project. + """ + meters = flask.request.storage_conn.get_meters(project=project) + return flask.jsonify(meters=list(meters)) + + +@blueprint.route('/sources//meters') +def list_meters_by_source(source): + """Return a list of meters by source. + + :param source: The ID of the owning source. + """ + meters = flask.request.storage_conn.get_meters(source=source) + return flask.jsonify(meters=list(meters)) + + ## APIs for working with resources. def _list_resources(source=None, user=None, project=None): diff --git a/ceilometer/storage/base.py b/ceilometer/storage/base.py index db009374..b4a6678d 100644 --- a/ceilometer/storage/base.py +++ b/ceilometer/storage/base.py @@ -133,3 +133,14 @@ class Connection(object): ( datetime.datetime(), datetime.datetime() ) """ + + @abc.abstractmethod + def get_meters(self, user=None, project=None, resource=None, source=None): + """Return a list of meters. + {'resource_id': UUID string for the resource, + 'project_id': UUID of project owning the resource, + 'user_id': UUID of user owning the resource, + 'name': The name of the meter, + 'type': The meter type (gauge, counter, diff), + } + """ diff --git a/ceilometer/storage/impl_log.py b/ceilometer/storage/impl_log.py index cfa00016..48f68921 100644 --- a/ceilometer/storage/impl_log.py +++ b/ceilometer/storage/impl_log.py @@ -80,6 +80,22 @@ class Connection(base.Connection): :param end_timestamp: Optional modified timestamp end range. """ + def get_meters(self, user=None, project=None, resource=None, source=None): + """Return an iterable of dictionaries containing meter information. + + { 'name': name of the meter, + 'type': type of the meter (guage, counter), + 'resource_id': UUID of the resource, + 'project_id': UUID of project owning the resource, + 'user_id': UUID of user owning the resource, + } + + :param user: Optional ID for user that owns the resource. + :param project: Optional ID for project that owns the resource. + :param resource: Optional ID of the resource. + :param source: Optional source filter. + """ + def get_raw_events(self, event_filter): """Return an iterable of raw event data as created by :func:`ceilometer.meter.meter_message_from_counter`. diff --git a/ceilometer/storage/impl_mongodb.py b/ceilometer/storage/impl_mongodb.py index 84a0eea6..5d5fdde1 100644 --- a/ceilometer/storage/impl_mongodb.py +++ b/ceilometer/storage/impl_mongodb.py @@ -22,7 +22,6 @@ import copy import datetime from ceilometer.openstack.common import log -from ceilometer.openstack.common import cfg from ceilometer.storage import base import bson.code @@ -348,6 +347,40 @@ class Connection(base.Connection): del r['_id'] yield r + def get_meters(self, user=None, project=None, resource=None, source=None): + """Return an iterable of dictionaries containing meter information. + + { 'name': name of the meter, + 'type': type of the meter (guage, counter), + 'resource_id': UUID of the resource, + 'project_id': UUID of project owning the resource, + 'user_id': UUID of user owning the resource, + } + + :param user: Optional ID for user that owns the resource. + :param project: Optional ID for project that owns the resource. + :param resource: Optional ID of the resource. + :param source: Optional source filter. + """ + q = {} + if user is not None: + q['user_id'] = user + if project is not None: + q['project_id'] = project + if resource is not None: + q['_id'] = resource + if source is not None: + q['source'] = source + for r in self.db.resource.find(q): + for r_meter in r['meter']: + m = {} + m['name'] = r_meter['counter_name'] + m['type'] = r_meter['counter_type'] + m['resource_id'] = r['_id'] + m['project_id'] = r['project_id'] + m['user_id'] = r['user_id'] + yield m + def get_raw_events(self, event_filter): """Return an iterable of raw event data as created by :func:`ceilometer.meter.meter_message_from_counter`. diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py index 1e07db36..92832c37 100644 --- a/ceilometer/storage/impl_sqlalchemy.py +++ b/ceilometer/storage/impl_sqlalchemy.py @@ -19,7 +19,7 @@ import copy import datetime -from ceilometer.openstack.common import cfg, log, timeutils +from ceilometer.openstack.common import log from ceilometer.storage import base from ceilometer.storage.sqlalchemy.models import Meter, Project, Resource from ceilometer.storage.sqlalchemy.models import Source, User @@ -257,6 +257,48 @@ class Connection(base.Connection): del r['meters'] yield r + def get_meters(self, user=None, project=None, source=None, + resource=None): + """Return an iterable of dictionaries containing meter information. + + { 'name': name of the meter, + 'type': type of the meter (guage, counter), + 'resource_id': UUID of the resource, + 'project_id': UUID of project owning the resource, + 'user_id': UUID of user owning the resource, + } + + :param user: Optional ID for user that owns the resource. + :param project: Optional ID for project that owns the resource. + :param resource: Optional ID of the resource. + :param source: Optional source filter. + """ + query = model_query(Resource, session=self.session) + if user is not None: + query = query.filter(Resource.user_id == user) + if source is not None: + query = query.filter(Resource.sources.any(id=source)) + if resource: + query = query.filter(Resource.id == resource) + if project is not None: + query = query.filter(Resource.project_id == project) + query = query.options( + sqlalchemy_session.sqlalchemy.orm.joinedload('meters')) + + for resource in query.all(): + meter_names = set() + for meter in resource.meters: + if meter.counter_name in meter_names: + continue + meter_names.add(meter.counter_name) + m = {} + m['resource_id'] = resource.id + m['project_id'] = resource.project_id + m['user_id'] = resource.user_id + m['name'] = meter.counter_name + m['type'] = meter.counter_type + yield m + def get_raw_events(self, event_filter): """Return an iterable of raw event data as created by :func:`ceilometer.meter.meter_message_from_counter`. diff --git a/doc/source/measurements.rst b/doc/source/measurements.rst index 699a0872..445ec6a4 100644 --- a/doc/source/measurements.rst +++ b/doc/source/measurements.rst @@ -117,6 +117,18 @@ storage.objects.size Gauge bytes store ID Total size of stor storage.objects.containers Gauge containers store ID Number of containers ========================== ========== ========== ======== ================================================== +Dynamically retrieving the Meters via ceilometer client +======================================================= + ceilometer meter-list -s openstack + +------------+-------+--------------------------------------+---------+----------------------------------+ + | Name | Type | Resource ID | User ID | Project ID | + +------------+-------+--------------------------------------+---------+----------------------------------+ + | image | gauge | 09e84d97-8712-4dd2-bcce-45970b2430f7 | | 57cf6d93688e4d39bf2fe3d3c03eb326 | + +The above command will retrieve the available meters that can be queried on +given the actual resource instances available. + + Naming convention ================= If you plan on adding meters, please follow the convention bellow: diff --git a/tests/api/v1/test_list_meters.py b/tests/api/v1/test_list_meters.py new file mode 100644 index 00000000..6dbecfc4 --- /dev/null +++ b/tests/api/v1/test_list_meters.py @@ -0,0 +1,155 @@ +# -*- encoding: utf-8 -*- +# +# Copyright 2012 Red Hat, Inc. +# +# Author: Angus Salkeld +# +# 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. +"""Test listing meters. +""" + +import datetime +import logging + +from ceilometer import counter +from ceilometer import meter +from ceilometer.openstack.common import cfg + +from ceilometer.tests import api as tests_api + +LOG = logging.getLogger(__name__) + + +class TestListEmptyMeters(tests_api.TestBase): + + def test_empty(self): + data = self.get('/meters') + self.assertEquals({'meters': []}, data) + + +class TestListMeters(tests_api.TestBase): + + def setUp(self): + super(TestListMeters, self).setUp() + + for cnt in [ + counter.Counter( + 'meter.test', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 10, 40), + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + }), + counter.Counter( + 'meter.test', + 'cumulative', + 3, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 11, 40), + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + }), + counter.Counter( + 'meter.mine', + 'gauge', + 1, + 'user-id', + 'project-id', + 'resource-id2', + timestamp=datetime.datetime(2012, 7, 2, 10, 41), + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter2', + }), + counter.Counter( + 'meter.test', + 'cumulative', + 1, + 'user-id2', + 'project-id2', + 'resource-id3', + timestamp=datetime.datetime(2012, 7, 2, 10, 42), + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter3', + }), + counter.Counter( + 'meter.mine', + 'gauge', + 1, + 'user-id4', + 'project-id2', + 'resource-id4', + timestamp=datetime.datetime(2012, 7, 2, 10, 43), + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter4', + })]: + msg = meter.meter_message_from_counter(cnt, + cfg.CONF.metering_secret, + 'test_list_resources') + self.conn.record_metering_data(msg) + + def test_list_meters(self): + data = self.get('/meters') + self.assertEquals(4, len(data['meters'])) + self.assertEquals(set(r['resource_id'] for r in data['meters']), + set(['resource-id', + 'resource-id2', + 'resource-id3', + 'resource-id4'])) + self.assertEquals(set(r['name'] for r in data['meters']), + set(['meter.test', + 'meter.mine'])) + + def test_with_resource(self): + data = self.get('/resources/resource-id/meters') + ids = set(r['name'] for r in data['meters']) + self.assertEquals(set(['meter.test']), ids) + + def test_with_source(self): + data = self.get('/sources/test_list_resources/meters') + ids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id', + 'resource-id2', + 'resource-id3', + 'resource-id4']), ids) + + def test_with_source_non_existent(self): + data = self.get('/sources/test_list_resources_dont_exist/meters') + self.assertEquals(data['meters'], []) + + def test_with_user(self): + data = self.get('/users/user-id/meters') + + nids = set(r['name'] for r in data['meters']) + self.assertEquals(set(['meter.mine', 'meter.test']), nids) + + rids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id', 'resource-id2']), rids) + + def test_with_user_non_existent(self): + data = self.get('/users/user-id-foobar123/meters') + self.assertEquals(data['meters'], []) + + def test_with_project(self): + data = self.get('/projects/project-id2/meters') + ids = set(r['resource_id'] for r in data['meters']) + self.assertEquals(set(['resource-id3', 'resource-id4']), ids) + + def test_with_project_non_existent(self): + data = self.get('/projects/jd-was-here/meters') + self.assertEquals(data['meters'], []) diff --git a/tests/storage/test_impl_mongodb.py b/tests/storage/test_impl_mongodb.py index 454278eb..7e6b5038 100644 --- a/tests/storage/test_impl_mongodb.py +++ b/tests/storage/test_impl_mongodb.py @@ -290,6 +290,18 @@ class MeterTest(MongoDBEngineTestBase): meter = self.db.meter.find_one() assert meter is not None + def test_get_meters(self): + results = list(self.conn.get_meters()) + assert len(results) == 4 + + def test_get_meters_by_user(self): + results = list(self.conn.get_meters(user='user-id')) + assert len(results) == 1 + + def test_get_meters_by_project(self): + results = list(self.conn.get_meters(project='project-id')) + assert len(results) == 2 + def test_get_raw_events_by_user(self): f = storage.EventFilter(user='user-id') results = list(self.conn.get_raw_events(f)) diff --git a/tests/storage/test_impl_sqlalchemy.py b/tests/storage/test_impl_sqlalchemy.py index 44a8eeaf..c1628fc6 100644 --- a/tests/storage/test_impl_sqlalchemy.py +++ b/tests/storage/test_impl_sqlalchemy.py @@ -326,6 +326,20 @@ class MeterTest(SQLAlchemyEngineTestBase): meter = self.session.query(Meter).first() assert meter is not None + def test_get_meters(self): + results = list(self.conn.get_meters()) + assert len(results) == 4 + + def test_get_meters_by_user(self): + results = list(self.conn.get_meters(user='user-id')) + assert len(results) == 1 + + def test_get_meters_by_project(self): + results = list(self.conn.get_meters(project='project-id')) + for r in results: + print r + assert len(results) == 2 + def test_get_raw_events_by_user(self): f = storage.EventFilter(user='user-id') results = list(self.conn.get_raw_events(f))