diff --git a/ceilometer/api/__init__.py b/ceilometer/api/__init__.py new file mode 100644 index 000000000..4c75f80c1 --- /dev/null +++ b/ceilometer/api/__init__.py @@ -0,0 +1,36 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. + +import flask.helpers + +from ceilometer.openstack.common import cfg +from ceilometer.openstack.common import jsonutils + +# Replace the json module used by flask with the one from +# openstack.common so we can take advantage of the fact that it knows +# how to serialize more complex objects. +flask.helpers.json = jsonutils + +# Register options for the service +API_SERVICE_OPTS = [ + cfg.IntOpt('metering_api_port', + default=9000, + help='The port for the ceilometer API server', + ), + ] +cfg.CONF.register_opts(API_SERVICE_OPTS) diff --git a/ceilometer/api/__main__.py b/ceilometer/api/__main__.py new file mode 100644 index 000000000..cc0ea04f2 --- /dev/null +++ b/ceilometer/api/__main__.py @@ -0,0 +1,27 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. +"""Set up the development API server. +""" + +from ceilometer.api.app import app +from ceilometer.openstack.common import cfg + +if __name__ == '__main__': + cfg.CONF() + app.debug = True + app.run(host='0.0.0.0', port=cfg.CONF.metering_api_port) diff --git a/ceilometer/api/app.py b/ceilometer/api/app.py new file mode 100644 index 000000000..e0c414a6c --- /dev/null +++ b/ceilometer/api/app.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. +"""Set up the API server application instance +""" + +import flask + +from ceilometer.openstack.common import cfg +from ceilometer import storage +from ceilometer.api import v1 + +app = flask.Flask('ceilometer.api') +app.register_blueprint(v1.blueprint, url_prefix='/v1') + +storage.register_opts(cfg.CONF) + + +@app.before_request +def attach_config(): + flask.request.cfg = cfg.CONF + storage_engine = storage.get_engine(cfg.CONF) + flask.request.storage_engine = storage_engine + flask.request.storage_conn = storage_engine.get_connection(cfg.CONF) + + +@app.route('/') +def hello(): + return 'Hello World!' diff --git a/ceilometer/api/v1.py b/ceilometer/api/v1.py new file mode 100644 index 000000000..1e4ab3720 --- /dev/null +++ b/ceilometer/api/v1.py @@ -0,0 +1,42 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. +"""Blueprint for version 1 of API. +""" + +import flask + + +blueprint = flask.Blueprint('v1', __name__) + +## APIs for working with resources. + + +@blueprint.route('/resources', defaults={'source': None}) +@blueprint.route('/sources//resources') +def list_resources(source): + resources = list(flask.request.storage_conn.get_resources(source=source)) + return flask.jsonify(resources=resources) + +## APIs for working with users. + + +@blueprint.route('/users', defaults={'source': None}) +@blueprint.route('/sources//users') +def list_users(source): + users = list(flask.request.storage_conn.get_users(source=source)) + return flask.jsonify(users=users) diff --git a/ceilometer/compute/network.py b/ceilometer/compute/network.py index a59b8b8bb..4f2218c42 100644 --- a/ceilometer/compute/network.py +++ b/ceilometer/compute/network.py @@ -20,8 +20,8 @@ from nova import exception from ceilometer.openstack.common import log -from .. import counter -from .. import plugin +from ceilometer import counter +from ceilometer import plugin class FloatingIPPollster(plugin.PollsterBase): diff --git a/ceilometer/tests/__init__.py b/ceilometer/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/ceilometer/tests/api.py b/ceilometer/tests/api.py new file mode 100644 index 000000000..13d738dcc --- /dev/null +++ b/ceilometer/tests/api.py @@ -0,0 +1,77 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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. +"""Base classes for API tests. +""" + +import json +import logging +import os +import unittest + +import flask +from ming import mim +import mock + +from ceilometer.api import v1 +from ceilometer.storage import impl_mongodb + +LOG = logging.getLogger(__name__) + + +class Connection(impl_mongodb.Connection): + + def _get_connection(self, conf): + # Use a real MongoDB server if we can connect, but fall back + # to a Mongo-in-memory connection if we cannot. + self.force_mongo = bool(int(os.environ.get('CEILOMETER_TEST_LIVE', 0))) + if self.force_mongo: + try: + return super(Connection, self)._get_connection(conf) + except: + LOG.debug('Unable to connect to mongod') + raise + else: + LOG.debug('Unable to connect to mongod, falling back to MIM') + return mim.Connection() + + +class TestBase(unittest.TestCase): + + def setUp(self): + super(TestBase, self).setUp() + self.app = flask.Flask('test') + self.app.register_blueprint(v1.blueprint) + self.test_app = self.app.test_client() + self.conf = mock.Mock() + self.conf.metering_storage_engine = 'mongodb' + self.conf.mongodb_host = 'localhost' + self.conf.mongodb_port = 27017 + self.conf.mongodb_dbname = 'testdb' + self.conn = Connection(self.conf) + self.conn.conn.drop_database('testdb') + self.conn.conn['testdb'] + + @self.app.before_request + def attach_storage_connection(): + flask.request.storage_conn = self.conn + return + + def get(self, path): + rv = self.test_app.get(path) + data = json.loads(rv.data) + return data diff --git a/doc/source/install.rst b/doc/source/install.rst index aeccc6716..e48976a10 100644 --- a/doc/source/install.rst +++ b/doc/source/install.rst @@ -19,12 +19,12 @@ Installing and Running the Development Version ================================================ -Ceilometer has two daemons. The :term:`agent` runs on the Nova compute -node(s) and the :term:`collector` runs on the cloud's management -node(s). In a development environment created by devstack_, these two -are typically the same server. They do not have to be, though, so some -of the instructions below are duplicated. Skip the steps you have -already done. +Ceilometer has three daemons. The :term:`agent` runs on the Nova +compute node(s). The :term:`collector` and API server run on the +cloud's management node(s). In a development environment created by +devstack_, these two are typically the same server. They do not have +to be, though, so some of the instructions below are duplicated. Skip +the steps you have already done. .. _devstack: http://www.devstack.org/ @@ -145,3 +145,33 @@ Installing the Compute Agent stderr, so you may want to run this step using a screen session or other tool for maintaining a long-running program in the background. + + +Installing the API Server +========================= + +.. index:: + double: installing; API + +1. Clone the ceilometer git repository to the server:: + + $ cd /opt/stack + $ git clone https://github.com/stackforge/ceilometer.git + +2. As a user with ``root`` permissions or ``sudo`` privileges, run the + ceilometer installer:: + + $ cd ceilometer + $ sudo python setup.py install + +3. Start the API server. + + :: + + $ python -m ceilometer.api + + .. note:: + + The development version of the API server logs to stderr, so you + may want to run this step using a screen session or other tool + for maintaining a long-running program in the background. diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/api/v1/__init__.py b/tests/api/v1/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/api/v1/test_list_resources.py b/tests/api/v1/test_list_resources.py new file mode 100644 index 000000000..6d5e0f24b --- /dev/null +++ b/tests/api/v1/test_list_resources.py @@ -0,0 +1,113 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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 resources. +""" + +import datetime +import logging + +from ceilometer import counter +from ceilometer import meter + +from ceilometer.tests import api as tests_api + +LOG = logging.getLogger(__name__) + + +class TestListResources(tests_api.TestBase): + + def test_empty(self): + data = self.get('/resources') + self.assertEquals({'resources': []}, data) + + def test_instances(self): + counter1 = counter.Counter( + 'test', + 'instance', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 10, 40), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + } + ) + msg = meter.meter_message_from_counter(counter1) + self.conn.record_metering_data(msg) + + counter2 = counter.Counter( + 'test', + 'instance', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id-alternate', + timestamp=datetime.datetime(2012, 7, 2, 10, 41), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter2', + } + ) + msg2 = meter.meter_message_from_counter(counter2) + self.conn.record_metering_data(msg2) + + data = self.get('/resources') + self.assertEquals(2, len(data['resources'])) + + def test_with_source(self): + counter1 = counter.Counter( + 'test_list_resources', + 'instance', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 10, 40), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + } + ) + msg = meter.meter_message_from_counter(counter1) + self.conn.record_metering_data(msg) + + counter2 = counter.Counter( + 'not-test', + 'instance', + 'cumulative', + 1, + 'user-id2', + 'project-id', + 'resource-id-alternate', + timestamp=datetime.datetime(2012, 7, 2, 10, 41), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter2', + } + ) + msg2 = meter.meter_message_from_counter(counter2) + self.conn.record_metering_data(msg2) + + data = self.get('/sources/test_list_resources/resources') + ids = [r['resource_id'] for r in data['resources']] + self.assertEquals(['resource-id'], ids) diff --git a/tests/api/v1/test_list_users.py b/tests/api/v1/test_list_users.py new file mode 100644 index 000000000..dc0dbee24 --- /dev/null +++ b/tests/api/v1/test_list_users.py @@ -0,0 +1,112 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# 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 users. +""" + +import datetime +import logging + +from ceilometer import counter +from ceilometer import meter + +from ceilometer.tests import api as tests_api + +LOG = logging.getLogger(__name__) + + +class TestListUsers(tests_api.TestBase): + + def test_empty(self): + data = self.get('/users') + self.assertEquals({'users': []}, data) + + def test_users(self): + counter1 = counter.Counter( + 'test_list_users', + 'instance', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 10, 40), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + } + ) + msg = meter.meter_message_from_counter(counter1) + self.conn.record_metering_data(msg) + + counter2 = counter.Counter( + 'test_list_users', + 'instance', + 'cumulative', + 1, + 'user-id2', + 'project-id', + 'resource-id-alternate', + timestamp=datetime.datetime(2012, 7, 2, 10, 41), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter2', + } + ) + msg2 = meter.meter_message_from_counter(counter2) + self.conn.record_metering_data(msg2) + + data = self.get('/users') + self.assertEquals(['user-id', 'user-id2'], data['users']) + + def test_with_source(self): + counter1 = counter.Counter( + 'test_list_users', + 'instance', + 'cumulative', + 1, + 'user-id', + 'project-id', + 'resource-id', + timestamp=datetime.datetime(2012, 7, 2, 10, 40), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter', + } + ) + msg = meter.meter_message_from_counter(counter1) + self.conn.record_metering_data(msg) + + counter2 = counter.Counter( + 'not-test', + 'instance', + 'cumulative', + 1, + 'user-id2', + 'project-id', + 'resource-id-alternate', + timestamp=datetime.datetime(2012, 7, 2, 10, 41), + duration=0, + resource_metadata={'display_name': 'test-server', + 'tag': 'self.counter2', + } + ) + msg2 = meter.meter_message_from_counter(counter2) + self.conn.record_metering_data(msg2) + + data = self.get('/sources/test_list_users/users') + self.assertEquals(['user-id'], data['users']) diff --git a/tools/pip-requires b/tools/pip-requires index c0d338ddc..f463bba3e 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -8,3 +8,4 @@ argparse sqlalchemy eventlet anyjson==0.3.1 +Flask==0.9 diff --git a/tools/pip-requires_essex b/tools/pip-requires_essex index eafa02807..9903ffb6a 100644 --- a/tools/pip-requires_essex +++ b/tools/pip-requires_essex @@ -8,4 +8,5 @@ lockfile netaddr argparse sqlalchemy -anyjson==0.3.1 \ No newline at end of file +anyjson==0.3.1 +Flask==0.9 diff --git a/tox.ini b/tox.ini index 94636a7c6..c3ce8c81d 100644 --- a/tox.ini +++ b/tox.ini @@ -14,7 +14,7 @@ commands = {toxinidir}/run_tests.sh sitepackages = True [testenv:py27] -commands = {toxinidir}/run_tests.sh --with-coverage --cover-erase --cover-package=ceilometer --cover-inclusive [] +commands = {toxinidir}/run_tests.sh --no-path-adjustment --with-coverage --cover-erase --cover-package=ceilometer --cover-inclusive [] [testenv:pep8] deps = pep8==1.1