Skeleton for API server

This changeset introduces a framework for the API service,
including the dependency list, a couple of simple API methods,
a test suite, and documentation for starting the development
server.

Change-Id: I4a496c600b7e6a0a8c70113b1d099614febd899d
Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
This commit is contained in:
Doug Hellmann 2012-07-25 14:09:34 -04:00
parent ab1437fbbc
commit 2eebd4a8bd
16 changed files with 492 additions and 10 deletions

View File

@ -0,0 +1,36 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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)

View File

@ -0,0 +1,27 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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)

43
ceilometer/api/app.py Normal file
View File

@ -0,0 +1,43 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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!'

42
ceilometer/api/v1.py Normal file
View File

@ -0,0 +1,42 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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/<source>/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/<source>/users')
def list_users(source):
users = list(flask.request.storage_conn.get_users(source=source))
return flask.jsonify(users=users)

View File

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

View File

77
ceilometer/tests/api.py Normal file
View File

@ -0,0 +1,77 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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

View File

@ -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.

0
tests/__init__.py Normal file
View File

0
tests/api/__init__.py Normal file
View File

0
tests/api/v1/__init__.py Normal file
View File

View File

@ -0,0 +1,113 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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)

View File

@ -0,0 +1,112 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Doug Hellmann <doug.hellmann@dreamhost.com>
#
# 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'])

View File

@ -8,3 +8,4 @@ argparse
sqlalchemy
eventlet
anyjson==0.3.1
Flask==0.9

View File

@ -8,4 +8,5 @@ lockfile
netaddr
argparse
sqlalchemy
anyjson==0.3.1
anyjson==0.3.1
Flask==0.9

View File

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