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))