Implement V2 API with Pecan and WSME

This changeset reimplements the API
using Pecan and WSME instead of Flask.

Pecan uses "object dispatch" instead of
declared routes. The controller classes
are chained together to implement the
API.  Most of what we have are simple
REST lookups, but a few cases required
custom methods.

WSME is used to define types of inputs
and outputs for each controller method.
The WSME layer handles serizlization and
deserialization in several formats. In
our case, only JSON and XML are
configured.

There are a few small changes to the
return types in the API, as well as to
error handling. Now all errors are
returned as JSON messages made up of a
mapping containing the key
'error_message' and the text of the
error. This will later be enhanced to
include XML support for XML requests.

This change also moves the script for
starting the V1 API to a new name and
replaces it with a script that starts
the V2 API. There is an open
bug/blueprint to fix that so both
versions of the API are loaded.

blueprint api-server-pecan-wsme

Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
Change-Id: I1b99a16de68f902370a8999eca073c56f9f14865
This commit is contained in:
Doug Hellmann 2012-12-03 12:57:36 -05:00
parent 42f1f02077
commit 3173ab4c4b
34 changed files with 2548 additions and 37 deletions

View File

@ -5,3 +5,4 @@ exclude .gitignore
exclude .gitreview exclude .gitreview
global-exclude *.pyc global-exclude *.pyc
recursive-include public *

View File

@ -18,10 +18,15 @@
# under the License. # under the License.
"""Set up the development API server. """Set up the development API server.
""" """
import os
import sys import sys
from wsgiref import simple_server
from pecan import configuration
from ceilometer.api import acl from ceilometer.api import acl
from ceilometer.api.v1 import app from ceilometer.api import app
from ceilometer.api import config as api_config
from ceilometer.openstack.common import cfg from ceilometer.openstack.common import cfg
from ceilometer.openstack.common import log as logging from ceilometer.openstack.common import log as logging
@ -32,15 +37,34 @@ if __name__ == '__main__':
# inputs. # inputs.
acl.register_opts(cfg.CONF) acl.register_opts(cfg.CONF)
# Parse config file and command line options, # Parse OpenStack config file and command line options, then
# then configure logging. # configure logging.
cfg.CONF(sys.argv[1:]) cfg.CONF(sys.argv[1:])
logging.setup('ceilometer.api') logging.setup('ceilometer.api')
root = app.make_app() # Set up the pecan configuration
filename = api_config.__file__.replace('.pyc', '.py')
pecan_config = configuration.conf_from_file(filename)
# Enable debug mode # Build the WSGI app
if cfg.CONF.verbose or cfg.CONF.debug: root = app.setup_app(pecan_config,
root.debug = True extra_hooks=[acl.AdminAuthHook()])
root = acl.install(root, cfg.CONF)
root.run(host='0.0.0.0', port=cfg.CONF.metering_api_port) # Create the WSGI server and start it
host, port = '0.0.0.0', int(cfg.CONF.metering_api_port)
srv = simple_server.make_server(host, port, root)
print 'Starting server in PID %s' % os.getpid()
if host == '0.0.0.0':
print 'serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' % \
(port, port)
else:
print "serving on http://%s:%s" % (host, port)
try:
srv.serve_forever()
except KeyboardInterrupt:
# allow CTRL+C to shutdown without an error
pass

46
bin/ceilometer-api-v1 Executable file
View File

@ -0,0 +1,46 @@
#!/usr/bin/env python
# -*- 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.
"""
import sys
from ceilometer.api import acl
from ceilometer.api.v1 import app
from ceilometer.openstack.common import cfg
from ceilometer.openstack.common import log as logging
if __name__ == '__main__':
# Register keystone middleware option before
# parsing the config file and command line
# inputs.
acl.register_opts(cfg.CONF)
# Parse config file and command line options,
# then configure logging.
cfg.CONF(sys.argv[1:])
logging.setup('ceilometer.api')
root = app.make_app()
# Enable debug mode
if cfg.CONF.verbose or cfg.CONF.debug:
root.debug = True
root.run(host='0.0.0.0', port=cfg.CONF.metering_api_port)

View File

@ -2,7 +2,7 @@
# #
# Copyright © 2012 New Dream Network, LLC (DreamHost) # Copyright © 2012 New Dream Network, LLC (DreamHost)
# #
# Author: Julien Danjou <julien@danjou.info> # Author: Doug Hellmann <doug.hellmann@dreamhost.com>
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # 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 # not use this file except in compliance with the License. You may obtain
@ -17,9 +17,12 @@
# under the License. # under the License.
"""Set up the ACL to acces the API server.""" """Set up the ACL to acces the API server."""
import flask
from ceilometer import policy from ceilometer import policy
from pecan import hooks
from webob import exc
import keystoneclient.middleware.auth_token as auth_token import keystoneclient.middleware.auth_token as auth_token
@ -34,17 +37,19 @@ def register_opts(conf):
def install(app, conf): def install(app, conf):
"""Install ACL check on application.""" """Install ACL check on application."""
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app, new_app = auth_token.AuthProtocol(app,
conf=conf, conf=conf,
) )
app.before_request(check) return new_app
return app
def check(): class AdminAuthHook(hooks.PecanHook):
"""Check application access.""" """Verify that the user has admin rights
headers = flask.request.headers """
if not policy.check_is_admin(headers.get('X-Roles', "").split(","),
headers.get('X-Tenant-Id'), def before(self, state):
headers.get('X-Tenant-Name')): headers = state.request.headers
return "Access denied", 401 if not policy.check_is_admin(headers.get('X-Roles', "").split(","),
headers.get('X-Tenant-Id'),
headers.get('X-Tenant-Name')):
raise exc.HTTPUnauthorized()

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

@ -0,0 +1,44 @@
# -*- 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.
from pecan import make_app
from ceilometer.api import hooks
from ceilometer.api import middleware
from ceilometer.service import prepare_service
def setup_app(config, extra_hooks=[]):
# Initialize the cfg.CONF object
prepare_service([])
# FIXME: Replace DBHook with a hooks.TransactionHook
app_hooks = [hooks.ConfigHook(),
hooks.DBHook()]
app_hooks.extend(extra_hooks)
return make_app(
config.app.root,
static_root=config.app.static_root,
template_path=config.app.template_path,
logging=getattr(config, 'logging', {}),
debug=getattr(config.app, 'debug', False),
force_canonical=getattr(config.app, 'force_canonical', True),
hooks=app_hooks,
wrap_app=middleware.ParsableErrorMiddleware,
)

41
ceilometer/api/config.py Normal file
View File

@ -0,0 +1,41 @@
# Server Specific Configurations
server = {
'port': '8080',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': 'ceilometer.api.controllers.root.RootController',
'modules': ['ceilometer.api'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/ceilometer/api/templates',
'debug': False,
}
logging = {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'ceilometer': {'level': 'DEBUG', 'handlers': ['console']}
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
},
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf

View File

View File

@ -0,0 +1,31 @@
# -*- 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.
from pecan import expose
from . import v2
class RootController(object):
v2 = v2.V2Controller()
@expose(generic=True, template='index.html')
def index(self):
# FIXME: Return version information
return dict()

View File

@ -0,0 +1,560 @@
# -*- 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.
"""Version 2 of the API.
"""
# [ ] / -- information about this version of the API
#
# [ ] /extensions -- list of available extensions
# [ ] /extensions/<extension> -- details about a specific extension
#
# [ ] /sources -- list of known sources (where do we get this?)
# [ ] /sources/components -- list of components which provide metering
# data (where do we get this)?
#
# [x] /projects/<project>/resources -- list of resource ids
# [x] /resources -- list of resource ids
# [x] /sources/<source>/resources -- list of resource ids
# [x] /users/<user>/resources -- list of resource ids
#
# [x] /users -- list of user ids
# [x] /sources/<source>/users -- list of user ids
#
# [x] /projects -- list of project ids
# [x] /sources/<source>/projects -- list of project ids
#
# [ ] /resources/<resource> -- metadata
#
# [ ] /projects/<project>/meters -- list of meters reporting for parent obj
# [ ] /resources/<resource>/meters -- list of meters reporting for parent obj
# [ ] /sources/<source>/meters -- list of meters reporting for parent obj
# [ ] /users/<user>/meters -- list of meters reporting for parent obj
#
# [x] /projects/<project>/meters/<meter> -- events
# [x] /resources/<resource>/meters/<meter> -- events
# [x] /sources/<source>/meters/<meter> -- events
# [x] /users/<user>/meters/<meter> -- events
#
# [ ] /projects/<project>/meters/<meter>/duration -- total time for selected
# meter
# [x] /resources/<resource>/meters/<meter>/duration -- total time for selected
# meter
# [ ] /sources/<source>/meters/<meter>/duration -- total time for selected
# meter
# [ ] /users/<user>/meters/<meter>/duration -- total time for selected meter
#
# [ ] /projects/<project>/meters/<meter>/volume -- total or max volume for
# selected meter
# [x] /projects/<project>/meters/<meter>/volume/max -- max volume for
# selected meter
# [x] /projects/<project>/meters/<meter>/volume/sum -- total volume for
# selected meter
# [ ] /resources/<resource>/meters/<meter>/volume -- total or max volume for
# selected meter
# [x] /resources/<resource>/meters/<meter>/volume/max -- max volume for
# selected meter
# [x] /resources/<resource>/meters/<meter>/volume/sum -- total volume for
# selected meter
# [ ] /sources/<source>/meters/<meter>/volume -- total or max volume for
# selected meter
# [ ] /users/<user>/meters/<meter>/volume -- total or max volume for selected
# meter
import datetime
import os
import pecan
from pecan import request
from pecan.rest import RestController
import wsme
import wsme.pecan
from wsme.types import Base, text, wsattr
from ceilometer.openstack.common import jsonutils
from ceilometer.openstack.common import log as logging
from ceilometer.openstack.common import timeutils
from ceilometer import storage
LOG = logging.getLogger(__name__)
def _get_query_timestamps(args={}):
"""Return any optional timestamp information in the request.
Determine the desired range, if any, from the GET arguments. Set
up the query range using the specified offset.
[query_start ... start_timestamp ... end_timestamp ... query_end]
Returns a dictionary containing:
query_start: First timestamp to use for query
start_timestamp: start_timestamp parameter from request
query_end: Final timestamp to use for query
end_timestamp: end_timestamp parameter from request
search_offset: search_offset parameter from request
"""
search_offset = int(args.get('search_offset', 0))
start_timestamp = args.get('start_timestamp')
if start_timestamp:
start_timestamp = timeutils.parse_isotime(start_timestamp)
start_timestamp = start_timestamp.replace(tzinfo=None)
query_start = (start_timestamp -
datetime.timedelta(minutes=search_offset))
else:
query_start = None
end_timestamp = args.get('end_timestamp')
if end_timestamp:
end_timestamp = timeutils.parse_isotime(end_timestamp)
end_timestamp = end_timestamp.replace(tzinfo=None)
query_end = end_timestamp + datetime.timedelta(minutes=search_offset)
else:
query_end = None
return {'query_start': query_start,
'query_end': query_end,
'start_timestamp': start_timestamp,
'end_timestamp': end_timestamp,
'search_offset': search_offset,
}
# FIXME(dhellmann): Change APIs that use this to return float?
class MeterVolume(Base):
volume = wsattr(float, mandatory=False)
def __init__(self, volume, **kw):
if volume is not None:
volume = float(volume)
super(MeterVolume, self).__init__(volume=volume, **kw)
class MeterVolumeController(object):
@wsme.pecan.wsexpose(MeterVolume)
def max(self):
"""Find the maximum volume for the matching meter events.
"""
q_ts = _get_query_timestamps(request.params)
try:
meter = request.context['meter_id']
except KeyError:
raise ValueError('No meter specified')
resource = request.context.get('resource_id')
project = request.context.get('project_id')
# Query the database for the max volume
f = storage.EventFilter(meter=meter,
resource=resource,
start=q_ts['query_start'],
end=q_ts['query_end'],
project=project,
)
# TODO(sberler): do we want to return an error if the resource
# does not exist?
results = list(request.storage_conn.get_volume_max(f))
value = None
if results:
if resource:
# If the caller specified a resource there should only
# be one result.
value = results[0].get('value')
else:
# FIXME(sberler): Currently get_volume_max is really
# always grouping by resource_id. We should add a new
# function in the storage driver that does not do this
# grouping (and potentially rename the existing one to
# get_volume_max_by_resource())
value = max(result.get('value') for result in results)
return MeterVolume(volume=value)
@wsme.pecan.wsexpose(MeterVolume)
def sum(self):
"""Compute the total volume for the matching meter events.
"""
q_ts = _get_query_timestamps(request.params)
try:
meter = request.context['meter_id']
except KeyError:
raise ValueError('No meter specified')
resource = request.context.get('resource_id')
project = request.context.get('project_id')
f = storage.EventFilter(meter=meter,
project=project,
start=q_ts['query_start'],
end=q_ts['query_end'],
resource=resource,
)
# TODO(sberler): do we want to return an error if the resource
# does not exist?
results = list(request.storage_conn.get_volume_sum(f))
value = None
if results:
if resource:
# If the caller specified a resource there should only
# be one result.
value = results[0].get('value')
else:
# FIXME(sberler): Currently get_volume_max is really
# always grouping by resource_id. We should add a new
# function in the storage driver that does not do this
# grouping (and potentially rename the existing one to
# get_volume_max_by_resource())
value = sum(result.get('value') for result in results)
return MeterVolume(volume=value)
class Event(Base):
source = text
counter_name = text
counter_type = text
counter_volume = float
user_id = text
project_id = text
resource_id = text
timestamp = datetime.datetime
# FIXME(dhellmann): Need to add the metadata back as
# a flat {text: text} mapping.
#resource_metadata = ?
message_id = text
def __init__(self, counter_volume=None, **kwds):
if counter_volume is not None:
counter_volume = float(counter_volume)
super(Event, self).__init__(counter_volume=counter_volume,
**kwds)
class Duration(Base):
start_timestamp = datetime.datetime
end_timestamp = datetime.datetime
duration = float
class MeterController(RestController):
"""Manages operations on a single meter.
"""
volume = MeterVolumeController()
_custom_actions = {
'duration': ['GET'],
}
def __init__(self, meter_id):
request.context['meter_id'] = meter_id
self._id = meter_id
@wsme.pecan.wsexpose([Event])
def get_all(self):
"""Return all events for the meter.
"""
q_ts = _get_query_timestamps(request.params)
f = storage.EventFilter(
user=request.context.get('user_id'),
project=request.context.get('project_id'),
start=q_ts['query_start'],
end=q_ts['query_end'],
resource=request.context.get('resource_id'),
meter=self._id,
source=request.context.get('source_id'),
)
return [Event(**e)
for e in request.storage_conn.get_raw_events(f)
]
@wsme.pecan.wsexpose(Duration)
def duration(self):
"""Computes the duration of the meter events in the time range given.
"""
q_ts = _get_query_timestamps(request.params)
start_timestamp = q_ts['start_timestamp']
end_timestamp = q_ts['end_timestamp']
# Query the database for the interval of timestamps
# within the desired range.
f = storage.EventFilter(user=request.context.get('user_id'),
project=request.context.get('project_id'),
start=q_ts['query_start'],
end=q_ts['query_end'],
resource=request.context.get('resource_id'),
meter=self._id,
source=request.context.get('source_id'),
)
min_ts, max_ts = request.storage_conn.get_event_interval(f)
# "Clamp" the timestamps we return to the original time
# range, excluding the offset.
LOG.debug('start_timestamp %s, end_timestamp %s, min_ts %s, max_ts %s',
start_timestamp, end_timestamp, min_ts, max_ts)
if start_timestamp and min_ts and min_ts < start_timestamp:
min_ts = start_timestamp
LOG.debug('clamping min timestamp to range')
if end_timestamp and max_ts and max_ts > end_timestamp:
max_ts = end_timestamp
LOG.debug('clamping max timestamp to range')
# If we got valid timestamps back, compute a duration in minutes.
#
# If the min > max after clamping then we know the
# timestamps on the events fell outside of the time
# range we care about for the query, so treat them as
# "invalid."
#
# If the timestamps are invalid, return None as a
# sentinal indicating that there is something "funny"
# about the range.
if min_ts and max_ts and (min_ts <= max_ts):
# Can't use timedelta.total_seconds() because
# it is not available in Python 2.6.
diff = max_ts - min_ts
duration = (diff.seconds + (diff.days * 24 * 60 ** 2)) / 60
else:
min_ts = max_ts = duration = None
return Duration(start_timestamp=min_ts,
end_timestamp=max_ts,
duration=duration,
)
class Meter(Base):
name = text
type = text
resource_id = text
project_id = text
user_id = text
class MetersController(RestController):
"""Works on meters."""
@pecan.expose()
def _lookup(self, meter_id, *remainder):
return MeterController(meter_id), remainder
@wsme.pecan.wsexpose([Meter])
def get_all(self):
user_id = request.context.get('user_id')
project_id = request.context.get('project_id')
resource_id = request.context.get('resource_id')
source_id = request.context.get('source_id')
return [Meter(**m)
for m in request.storage_conn.get_meters(user=user_id,
project=project_id,
resource=resource_id,
source=source_id,
)]
class ResourceController(RestController):
"""Manages operations on a single resource.
"""
def __init__(self, resource_id):
request.context['resource_id'] = resource_id
meters = MetersController()
class MeterDescription(Base):
counter_name = text
counter_type = text
class Resource(Base):
resource_id = text
project_id = text
user_id = text
timestamp = datetime.datetime
#metadata = ?
meter = wsattr([MeterDescription])
def __init__(self, meter=[], **kwds):
meter = [MeterDescription(**m) for m in meter]
super(Resource, self).__init__(meter=meter, **kwds)
class ResourcesController(RestController):
"""Works on resources."""
@pecan.expose()
def _lookup(self, resource_id, *remainder):
return ResourceController(resource_id), remainder
@wsme.pecan.wsexpose([Resource])
def get_all(self, start_timestamp=None, end_timestamp=None):
if start_timestamp:
start_timestamp = timeutils.parse_isotime(start_timestamp)
if end_timestamp:
end_timestamp = timeutils.parse_isotime(end_timestamp)
resources = [
Resource(**r)
for r in request.storage_conn.get_resources(
source=request.context.get('source_id'),
user=request.context.get('user_id'),
project=request.context.get('project_id'),
start_timestamp=start_timestamp,
end_timestamp=end_timestamp,
)]
return resources
class ProjectController(RestController):
"""Works on resources."""
def __init__(self, project_id):
request.context['project_id'] = project_id
meters = MetersController()
resources = ResourcesController()
class ProjectsController(RestController):
"""Works on projects."""
@pecan.expose()
def _lookup(self, project_id, *remainder):
return ProjectController(project_id), remainder
@wsme.pecan.wsexpose([text])
def get_all(self):
source_id = request.context.get('source_id')
projects = list(request.storage_conn.get_projects(source=source_id))
return projects
meters = MetersController()
class UserController(RestController):
"""Works on reusers."""
def __init__(self, user_id):
request.context['user_id'] = user_id
meters = MetersController()
resources = ResourcesController()
class UsersController(RestController):
"""Works on users."""
@pecan.expose()
def _lookup(self, user_id, *remainder):
return UserController(user_id), remainder
@wsme.pecan.wsexpose([text])
def get_all(self):
source_id = request.context.get('source_id')
users = list(request.storage_conn.get_users(source=source_id))
return users
class Source(Base):
name = text
data = {text: text}
class SourceController(RestController):
"""Works on resources."""
def __init__(self, source_id, data):
request.context['source_id'] = source_id
self._id = source_id
self._data = data
@wsme.pecan.wsexpose(Source)
def get(self):
response = Source(name=self._id, data=self._data)
print 'RETURNING:', response
return response
meters = MetersController()
resources = ResourcesController()
projects = ProjectsController()
users = UsersController()
class SourcesController(RestController):
"""Works on sources."""
def __init__(self):
self._sources = None
@property
def sources(self):
# FIXME(dhellmann): Add a configuration option for the filename.
#
# FIXME(dhellmann): We only want to load the file once in a process,
# but we want to be able to mock the loading out in separate tests.
#
if not self._sources:
self._sources = self._load_sources(os.path.abspath("sources.json"))
return self._sources
@staticmethod
def _load_sources(filename):
try:
with open(filename, "r") as f:
sources = jsonutils.load(f)
except IOError as err:
LOG.warning('Could not load data source definitions from %s: %s' %
(filename, err))
sources = {}
return sources
@pecan.expose()
def _lookup(self, source_id, *remainder):
try:
data = self.sources[source_id]
except KeyError:
# Unknown source
pecan.abort(404, detail='No source %s' % source_id)
return SourceController(source_id, data), remainder
@wsme.pecan.wsexpose([Source])
def get_all(self):
return [Source(name=key, data=value)
for key, value in self.sources.iteritems()]
class V2Controller(object):
"""Version 2 API controller root."""
projects = ProjectsController()
resources = ResourcesController()
sources = SourcesController()
users = UsersController()
meters = MetersController()

44
ceilometer/api/hooks.py Normal file
View File

@ -0,0 +1,44 @@
# -*- 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.
from pecan import hooks
from ceilometer.openstack.common import cfg
from ceilometer import storage
class ConfigHook(hooks.PecanHook):
"""Attach the configuration object to the request
so controllers can get to it.
"""
def before(self, state):
state.request.cfg = cfg.CONF
class DBHook(hooks.PecanHook):
def before(self, state):
storage_engine = storage.get_engine(state.request.cfg)
state.request.storage_engine = storage_engine
state.request.storage_conn = storage_engine.get_connection(
state.request.cfg)
# def after(self, state):
# print 'method:', state.request.method
# print 'response:', state.response.status

View File

@ -0,0 +1,74 @@
# -*- 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.
"""Middleware to replace the plain text message body of an error
response with one formatted so the client can parse it.
Based on pecan.middleware.errordocument
"""
import json
from webob import exc
class ParsableErrorMiddleware(object):
"""Replace error body with something the client can parse.
"""
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
# Request for this state, modified by replace_start_response()
# and used when an error is being reported.
state = {}
def replacement_start_response(status, headers, exc_info=None):
"""Overrides the default response to make errors parsable.
"""
try:
status_code = int(status.split(' ')[0])
state['status_code'] = status_code
except (ValueError, TypeError): # pragma: nocover
raise Exception((
'ErrorDocumentMiddleware received an invalid '
'status %s' % status
))
else:
if (state['status_code'] / 100) not in (2, 3):
# Remove some headers so we can replace them later
# when we have the full error message and can
# compute the length.
headers = [(h, v)
for (h, v) in headers
if h not in ('Content-Length', 'Content-Type')
]
# Save the headers in case we need to modify them.
state['headers'] = headers
return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response)
if (state['status_code'] / 100) not in (2, 3):
# FIXME(dhellmann): Always returns errors as JSON,
# but should look at the environ to determine
# the desired type.
body = [json.dumps({'error_message': '\n'.join(app_iter)})]
state['headers'].append(('Content-Length', len(body[0])))
state['headers'].append(('Content-Type', 'application/json'))
else:
body = app_iter
return body

50
ceilometer/api/v1/acl.py Normal file
View File

@ -0,0 +1,50 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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 ACL to acces the API server."""
import flask
from ceilometer import policy
import keystoneclient.middleware.auth_token as auth_token
def register_opts(conf):
"""Register keystoneclient middleware options
"""
conf.register_opts(auth_token.opts,
group='keystone_authtoken',
)
auth_token.CONF = conf
def install(app, conf):
"""Install ACL check on application."""
app.wsgi_app = auth_token.AuthProtocol(app.wsgi_app,
conf=conf,
)
app.before_request(check)
return app
def check():
"""Check application access."""
headers = flask.request.headers
if not policy.check_is_admin(headers.get('X-Roles', "").split(","),
headers.get('X-Tenant-Id'),
headers.get('X-Tenant-Name')):
return "Access denied", 401

View File

@ -20,10 +20,10 @@
This driver is based on MIM, an in-memory version of MongoDB. This driver is based on MIM, an in-memory version of MongoDB.
""" """
import logging
from ming import mim from ming import mim
from ceilometer.openstack.common import log as logging
from ceilometer.storage import base from ceilometer.storage import base
from ceilometer.storage import impl_mongodb from ceilometer.storage import impl_mongodb

View File

@ -19,16 +19,28 @@
""" """
import json import json
import os
import urllib import urllib
import unittest
import flask import flask
from pecan import set_config
from pecan.testing import load_test_app
from ceilometer.tests import db as db_test_base import mox
from ceilometer.api.v1 import blueprint as v1_blueprint import stubout
from ceilometer import storage
from ceilometer.api.v1 import app as v1_app from ceilometer.api.v1 import app as v1_app
from ceilometer.api.v1 import blueprint as v1_blueprint
from ceilometer.api.controllers import v2
from ceilometer.openstack.common import cfg
from ceilometer.tests import db as db_test_base
class TestBase(db_test_base.TestBase): class TestBase(db_test_base.TestBase):
"""Use only for v1 API tests.
"""
def setUp(self): def setUp(self):
super(TestBase, self).setUp() super(TestBase, self).setUp()
@ -52,3 +64,102 @@ class TestBase(db_test_base.TestBase):
print 'RAW DATA:', rv print 'RAW DATA:', rv
raise raise
return data return data
class FunctionalTest(unittest.TestCase):
"""
Used for functional tests of Pecan controllers where you need to
test your literal application and its integration with the
framework.
"""
DBNAME = 'testdb'
PATH_PREFIX = ''
SOURCE_DATA = {'test_source': {'somekey': '666'}}
def setUp(self):
cfg.CONF.database_connection = 'test://localhost/%s' % self.DBNAME
self.conn = storage.get_connection(cfg.CONF)
# Don't want to use drop_database() because we
# may end up running out of spidermonkey instances.
# http://davisp.lighthouseapp.com/projects/26898/tickets/22
self.conn.conn[self.DBNAME].clear()
# Determine where we are so we can set up paths in the config
root_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
'..',
'..',
)
)
self.config = {
'app': {
'root': 'ceilometer.api.controllers.root.RootController',
'modules': ['ceilometer.api'],
'static_root': '%s/public' % root_dir,
'template_path': '%s/ceilometer/api/templates' % root_dir,
},
'logging': {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'ceilometer': {'level': 'DEBUG',
'handlers': ['console'],
},
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
},
},
}
self.mox = mox.Mox()
self.stubs = stubout.StubOutForTesting()
self.app = self._make_app()
self._stubout_sources()
def _make_app(self):
return load_test_app(self.config)
def _stubout_sources(self):
"""Source data is usually read from a file, but
we want to let tests define their own. The class
attribute SOURCE_DATA is injected into the controller
as though it was read from the usual configuration
file.
"""
self.stubs.SmartSet(v2.SourcesController, 'sources',
self.SOURCE_DATA)
def tearDown(self):
self.mox.UnsetStubs()
self.stubs.UnsetAll()
self.stubs.SmartUnsetAll()
self.mox.VerifyAll()
set_config({}, overwrite=True)
def get_json(self, path, expect_errors=False, headers=None, **params):
full_path = self.PATH_PREFIX + path
print 'GET: %s %r' % (full_path, params)
response = self.app.get(full_path,
params=params,
headers=headers,
expect_errors=expect_errors)
if not expect_errors:
response = response.json
print 'GOT:', response
return response

View File

@ -18,7 +18,6 @@
"""Base classes for API tests. """Base classes for API tests.
""" """
import logging
import os import os
from ming import mim from ming import mim
@ -27,6 +26,7 @@ import mock
from nose.plugins import skip from nose.plugins import skip
from ceilometer.openstack.common import log as logging
from ceilometer.storage import impl_mongodb from ceilometer.storage import impl_mongodb
from ceilometer.tests import base as test_base from ceilometer.tests import base as test_base

View File

@ -18,8 +18,7 @@
"""Test ACL.""" """Test ACL."""
from ceilometer.tests import api as tests_api from ceilometer.tests import api as tests_api
from ceilometer.api import acl from ceilometer.api.v1 import acl
from ceilometer.openstack.common import cfg
class TestAPIACL(tests_api.TestBase): class TestAPIACL(tests_api.TestBase):
@ -42,14 +41,18 @@ class TestAPIACL(tests_api.TestBase):
self.app.preprocess_request() self.app.preprocess_request()
self.assertEqual(self.test_app.get().status_code, 401) self.assertEqual(self.test_app.get().status_code, 401)
def test_authenticated_wrong_tenant(self): # FIXME(dhellmann): This test is not properly looking at the tenant
with self.app.test_request_context('/', headers={ # info. The status code returned is the expected value, but it
"X-Roles": "admin", # is not clear why.
"X-Tenant-Name": "foobar", #
"X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb", # def test_authenticated_wrong_tenant(self):
}): # with self.app.test_request_context('/', headers={
self.app.preprocess_request() # "X-Roles": "admin",
self.assertEqual(self.test_app.get().status_code, 401) # "X-Tenant-Name": "foobar",
# "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
# }):
# self.app.preprocess_request()
# self.assertEqual(self.test_app.get().status_code, 401)
def test_authenticated(self): def test_authenticated(self):
with self.app.test_request_context('/', headers={ with self.app.test_request_context('/', headers={

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

30
tests/api/v2/base.py Normal file
View File

@ -0,0 +1,30 @@
# -*- 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.
from ceilometer.tests import api
class FunctionalTest(api.FunctionalTest):
PATH_PREFIX = '/v2'
def setUp(self):
super(FunctionalTest, self).setUp()
def tearDown(self):
super(FunctionalTest, self).tearDown()

82
tests/api/v2/test_acl.py Normal file
View File

@ -0,0 +1,82 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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 ACL."""
from ceilometer.api import acl
from ceilometer.api import app
from .base import FunctionalTest
class TestAPIACL(FunctionalTest):
def _make_app(self):
# Save the original app constructor so
# we can use it in our wrapper
original_setup_app = app.setup_app
# Wrap application construction with
# a function that ensures the AdminAuthHook
# is provided.
def setup_app(config, extra_hooks=[]):
extra_hooks = extra_hooks[:]
extra_hooks.append(acl.AdminAuthHook())
return original_setup_app(config, extra_hooks)
self.stubs.Set(app, 'setup_app', setup_app)
result = super(TestAPIACL, self)._make_app()
acl.install(result, {})
return result
def test_non_authenticated(self):
response = self.get_json('/sources', expect_errors=True)
self.assertEqual(response.status_code, 401)
def test_authenticated_wrong_role(self):
response = self.get_json('/sources',
expect_errors=True,
headers={
"X-Roles": "Member",
"X-Tenant-Name": "admin",
"X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
})
self.assertEqual(response.status_code, 401)
# FIXME(dhellmann): This test is not properly looking at the tenant
# info. We do not correctly detect the improper tenant. That's
# really something the keystone middleware would have to do using
# the incoming token, which we aren't providing.
#
# def test_authenticated_wrong_tenant(self):
# response = self.get_json('/sources',
# expect_errors=True,
# headers={
# "X-Roles": "admin",
# "X-Tenant-Name": "achoo",
# "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
# })
# self.assertEqual(response.status_code, 401)
def test_authenticated(self):
response = self.get_json('/sources',
expect_errors=True,
headers={
"X-Roles": "admin",
"X-Tenant-Name": "admin",
"X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
})
self.assertEqual(response.status_code, 200)

View File

@ -0,0 +1,143 @@
# -*- 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 raw events.
"""
import datetime
import logging
from ceilometer.openstack.common import timeutils
from ceilometer.storage import impl_test
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestComputeDurationByResource(FunctionalTest):
def setUp(self):
super(TestComputeDurationByResource, self).setUp()
# Create events relative to the range and pretend
# that the intervening events exist.
self.early1 = datetime.datetime(2012, 8, 27, 7, 0)
self.early2 = datetime.datetime(2012, 8, 27, 17, 0)
self.start = datetime.datetime(2012, 8, 28, 0, 0)
self.middle1 = datetime.datetime(2012, 8, 28, 8, 0)
self.middle2 = datetime.datetime(2012, 8, 28, 18, 0)
self.end = datetime.datetime(2012, 8, 28, 23, 59)
self.late1 = datetime.datetime(2012, 8, 29, 9, 0)
self.late2 = datetime.datetime(2012, 8, 29, 19, 0)
def _stub_interval_func(self, func):
self.stubs.Set(impl_test.TestConnection,
'get_event_interval',
func)
def _set_interval(self, start, end):
def get_interval(ignore_self, event_filter):
assert event_filter.start
assert event_filter.end
return (start, end)
self._stub_interval_func(get_interval)
def _invoke_api(self):
return self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
start_timestamp=self.start.isoformat(),
end_timestamp=self.end.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
def test_before_range(self):
self._set_interval(self.early1, self.early2)
data = self._invoke_api()
assert data['start_timestamp'] is None
assert data['end_timestamp'] is None
assert data['duration'] is None
def _assert_times_match(self, actual, expected):
#import pdb; pdb.set_trace()
if actual:
actual = timeutils.parse_isotime(actual)
actual = actual.replace(tzinfo=None)
assert actual == expected
def test_overlap_range_start(self):
self._set_interval(self.early1, self.middle1)
data = self._invoke_api()
self._assert_times_match(data['start_timestamp'], self.start)
self._assert_times_match(data['end_timestamp'], self.middle1)
assert data['duration'] == 8 * 60
def test_within_range(self):
self._set_interval(self.middle1, self.middle2)
data = self._invoke_api()
self._assert_times_match(data['start_timestamp'], self.middle1)
self._assert_times_match(data['end_timestamp'], self.middle2)
assert data['duration'] == 10 * 60
def test_within_range_zero_duration(self):
self._set_interval(self.middle1, self.middle1)
data = self._invoke_api()
self._assert_times_match(data['start_timestamp'], self.middle1)
self._assert_times_match(data['end_timestamp'], self.middle1)
assert data['duration'] == 0
def test_overlap_range_end(self):
self._set_interval(self.middle2, self.late1)
data = self._invoke_api()
self._assert_times_match(data['start_timestamp'], self.middle2)
self._assert_times_match(data['end_timestamp'], self.end)
assert data['duration'] == (6 * 60) - 1
def test_after_range(self):
self._set_interval(self.late1, self.late2)
data = self._invoke_api()
assert data['start_timestamp'] is None
assert data['end_timestamp'] is None
assert data['duration'] is None
def test_without_end_timestamp(self):
def get_interval(ignore_self, event_filter):
return (self.late1, self.late2)
self._stub_interval_func(get_interval)
data = self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
start_timestamp=self.late1.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
self._assert_times_match(data['start_timestamp'], self.late1)
self._assert_times_match(data['end_timestamp'], self.late2)
def test_without_start_timestamp(self):
def get_interval(ignore_self, event_filter):
return (self.early1, self.early2)
self._stub_interval_func(get_interval)
data = self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
end_timestamp=self.early2.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
self._assert_times_match(data['start_timestamp'], self.early1)
self._assert_times_match(data['end_timestamp'], self.early2)

View File

@ -0,0 +1,80 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Steven Berler <steven.berler@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 the _get_query_timestamps helper function.
"""
import unittest
import datetime
from ceilometer.api.controllers import v2 as api
class TimestampTest(unittest.TestCase):
def test_get_query_timestamps_none_specified(self):
result = api._get_query_timestamps()
expected = {'start_timestamp': None,
'end_timestamp': None,
'query_start': None,
'query_end': None,
'search_offset': 0,
}
assert result == expected
def test_get_query_timestamps_start(self):
args = {'start_timestamp': '2012-09-20T12:13:14'}
result = api._get_query_timestamps(args)
expected = {
'start_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
'end_timestamp': None,
'query_start': datetime.datetime(2012, 9, 20, 12, 13, 14),
'query_end': None,
'search_offset': 0,
}
assert result == expected
def test_get_query_timestamps_end(self):
args = {'end_timestamp': '2012-09-20T12:13:14'}
result = api._get_query_timestamps(args)
expected = {
'end_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
'start_timestamp': None,
'query_end': datetime.datetime(2012, 9, 20, 12, 13, 14),
'query_start': None,
'search_offset': 0,
}
assert result == expected
def test_get_query_timestamps_with_offset(self):
args = {'start_timestamp': '2012-09-20T12:13:14',
'end_timestamp': '2012-09-20T13:24:25',
'search_offset': '20',
}
result = api._get_query_timestamps(args)
expected = {
'query_end': datetime.datetime(2012, 9, 20, 13, 44, 25),
'query_start': datetime.datetime(2012, 9, 20, 11, 53, 14),
'end_timestamp': datetime.datetime(2012, 9, 20, 13, 24, 25),
'start_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
'search_offset': 20,
}
assert result == expected

View File

@ -0,0 +1,108 @@
# -*- 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 raw events.
"""
import datetime
import logging
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestListEvents(FunctionalTest):
def setUp(self):
super(TestListEvents, self).setUp()
self.counter1 = counter.Counter(
'instance',
'cumulative',
1,
'user-id',
'project1',
'resource-id',
timestamp=datetime.datetime(2012, 7, 2, 10, 40),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter',
}
)
msg = meter.meter_message_from_counter(self.counter1,
cfg.CONF.metering_secret,
'test_source',
)
self.conn.record_metering_data(msg)
self.counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project2',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(self.counter2,
cfg.CONF.metering_secret,
'source2',
)
self.conn.record_metering_data(msg2)
def test_all(self):
data = self.get_json('/resources')
self.assertEquals(2, len(data))
def test_empty_project(self):
data = self.get_json('/projects/no-such-project/meters/instance')
self.assertEquals([], data)
def test_by_project(self):
data = self.get_json('/projects/project1/meters/instance')
self.assertEquals(1, len(data))
def test_empty_resource(self):
data = self.get_json('/resources/no-such-resource/meters/instance')
self.assertEquals([], data)
def test_by_resource(self):
data = self.get_json('/resources/resource-id/meters/instance')
self.assertEquals(1, len(data))
def test_empty_source(self):
data = self.get_json('/sources/no-such-source/meters/instance',
expect_errors=True)
self.assertEquals(data.status_code, 404)
def test_by_source(self):
data = self.get_json('/sources/test_source/meters/instance')
self.assertEquals(1, len(data))
def test_empty_user(self):
data = self.get_json('/users/no-such-user/meters/instance')
self.assertEquals([], data)
def test_by_user(self):
data = self.get_json('/users/user-id/meters/instance')
self.assertEquals(1, len(data))

View File

@ -0,0 +1,157 @@
# -*- encoding: utf-8 -*-
#
# Copyright 2012 Red Hat, Inc.
#
# Author: Angus Salkeld <asalkeld@redhat.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 meters.
"""
import datetime
import logging
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestListEmptyMeters(FunctionalTest):
def test_empty(self):
data = self.get_json('/meters')
self.assertEquals([], data)
class TestListMeters(FunctionalTest):
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_source')
self.conn.record_metering_data(msg)
def test_list_meters(self):
data = self.get_json('/meters')
self.assertEquals(4, len(data))
self.assertEquals(set(r['resource_id'] for r in data),
set(['resource-id',
'resource-id2',
'resource-id3',
'resource-id4']))
self.assertEquals(set(r['name'] for r in data),
set(['meter.test',
'meter.mine']))
def test_with_resource(self):
data = self.get_json('/resources/resource-id/meters')
ids = set(r['name'] for r in data)
self.assertEquals(set(['meter.test']), ids)
def test_with_source(self):
data = self.get_json('/sources/test_source/meters')
ids = set(r['resource_id'] for r in data)
self.assertEquals(set(['resource-id',
'resource-id2',
'resource-id3',
'resource-id4']), ids)
def test_with_source_non_existent(self):
data = self.get_json('/sources/test_source_doesnt_exist/meters',
expect_errors=True)
self.assert_('No source test_source_doesnt_exist' in
data.json['error_message'])
def test_with_user(self):
data = self.get_json('/users/user-id/meters')
nids = set(r['name'] for r in data)
self.assertEquals(set(['meter.mine', 'meter.test']), nids)
rids = set(r['resource_id'] for r in data)
self.assertEquals(set(['resource-id', 'resource-id2']), rids)
def test_with_user_non_existent(self):
data = self.get_json('/users/user-id-foobar123/meters')
self.assertEquals(data, [])
def test_with_project(self):
data = self.get_json('/projects/project-id2/meters')
ids = set(r['resource_id'] for r in data)
self.assertEquals(set(['resource-id3', 'resource-id4']), ids)
def test_with_project_non_existent(self):
data = self.get_json('/projects/jd-was-here/meters')
self.assertEquals(data, [])

View File

@ -0,0 +1,118 @@
# -*- 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.openstack.common import cfg
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestListProjects(FunctionalTest):
def test_empty(self):
data = self.get_json('/projects')
self.assertEquals([], data)
def test_projects(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_source',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id2',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'test_source',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/projects')
self.assertEquals(['project-id', 'project-id2'], data)
def test_with_source(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_source',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id2',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'not-test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/sources/test_source/projects')
self.assertEquals(['project-id'], data)

View File

@ -0,0 +1,202 @@
# -*- 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.openstack.common import cfg
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestListResources(FunctionalTest):
SOURCE_DATA = {'test_list_resources': {}}
def test_empty(self):
data = self.get_json('/resources')
self.assertEquals([], data)
def test_instances(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/resources')
self.assertEquals(2, len(data))
def test_with_source(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_list_resources',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'not-test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/sources/test_list_resources/resources')
ids = [r['resource_id'] for r in data]
self.assertEquals(['resource-id'], ids)
def test_with_user(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_list_resources',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'not-test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/users/user-id/resources')
ids = [r['resource_id'] for r in data]
self.assertEquals(['resource-id'], ids)
def test_with_project(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_list_resources',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id2',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'not-test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/projects/project-id/resources')
ids = [r['resource_id'] for r in data]
self.assertEquals(['resource-id'], ids)

View File

@ -0,0 +1,47 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 Julien Danjou
#
# Author: Julien Danjou <julien@danjou.info>
#
# 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.
"""
from .base import FunctionalTest
class TestListSource(FunctionalTest):
def test_all(self):
ydata = self.get_json('/sources')
self.assertEqual(len(ydata), 1)
source = ydata[0]
self.assertEqual(source['name'], 'test_source')
def test_source(self):
ydata = self.get_json('/sources/test_source')
self.assert_("data" in ydata)
self.assert_("somekey" in ydata['data'])
self.assertEqual(ydata['data']["somekey"], '666')
def test_unknownsource(self):
ydata = self.get_json(
'/sources/test_source_that_does_not_exist',
expect_errors=True)
print 'GOT:', ydata
self.assertEqual(ydata.status_code, 404)
self.assert_(
"No source test_source_that_does_not_exist" in
ydata.json['error_message']
)

View File

@ -0,0 +1,120 @@
# -*- 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.openstack.common import cfg
from .base import FunctionalTest
LOG = logging.getLogger(__name__)
class TestListUsers(FunctionalTest):
SOURCE_DATA = {'test_list_users': {}}
def test_empty(self):
data = self.get_json('/users')
self.assertEquals([], data)
def test_users(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_list_users',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'test_list_users',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/users')
self.assertEquals(['user-id', 'user-id2'], data)
def test_with_source(self):
counter1 = counter.Counter(
'instance',
'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',
}
)
msg = meter.meter_message_from_counter(counter1,
cfg.CONF.metering_secret,
'test_list_users',
)
self.conn.record_metering_data(msg)
counter2 = counter.Counter(
'instance',
'cumulative',
1,
'user-id2',
'project-id',
'resource-id-alternate',
timestamp=datetime.datetime(2012, 7, 2, 10, 41),
resource_metadata={'display_name': 'test-server',
'tag': 'self.counter2',
}
)
msg2 = meter.meter_message_from_counter(counter2,
cfg.CONF.metering_secret,
'not-test',
)
self.conn.record_metering_data(msg2)
data = self.get_json('/sources/test_list_users/users')
self.assertEquals(['user-id'], data)

View File

@ -0,0 +1,95 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Steven Berler <steven.berler@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 getting the max resource volume.
"""
import datetime
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from ceilometer.tests.db import require_map_reduce
from .base import FunctionalTest
class TestMaxProjectVolume(FunctionalTest):
PATH = '/projects/project1/meters/volume.size/volume/max'
def setUp(self):
super(TestMaxProjectVolume, self).setUp()
require_map_reduce(self.conn)
self.counters = []
for i in range(3):
c = counter.Counter(
'volume.size',
'gauge',
5 + i,
'user-id',
'project1',
'resource-id-%s' % i,
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
resource_metadata={'display_name': 'test-volume',
'tag': 'self.counter',
}
)
self.counters.append(c)
msg = meter.meter_message_from_counter(c,
cfg.CONF.metering_secret,
'source1',
)
self.conn.record_metering_data(msg)
def test_no_time_bounds(self):
data = self.get_json(self.PATH)
expected = {'volume': 7}
self.assertEqual(data, expected)
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
expected = {'volume': 7}
self.assertEqual(data, expected)
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
expected = {'volume': None}
self.assertEqual(data, expected)
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
expected = {'volume': 5}
self.assertEqual(data, expected)
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
expected = {'volume': None}
self.assertEqual(data, expected)
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
expected = {'volume': 6}
self.assertEqual(data, expected)

View File

@ -0,0 +1,94 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Steven Berler <steven.berler@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 getting the max resource volume.
"""
import datetime
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from .base import FunctionalTest
from ceilometer.tests.db import require_map_reduce
class TestMaxResourceVolume(FunctionalTest):
PATH = '/resources/resource-id/meters/volume.size/volume/max'
def setUp(self):
super(TestMaxResourceVolume, self).setUp()
require_map_reduce(self.conn)
self.counters = []
for i in range(3):
c = counter.Counter(
'volume.size',
'gauge',
5 + i,
'user-id',
'project1',
'resource-id',
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
resource_metadata={'display_name': 'test-volume',
'tag': 'self.counter',
}
)
self.counters.append(c)
msg = meter.meter_message_from_counter(c,
cfg.CONF.metering_secret,
'source1',
)
self.conn.record_metering_data(msg)
def test_no_time_bounds(self):
data = self.get_json(self.PATH)
expected = {'volume': 7}
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
expected = {'volume': 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
expected = {'volume': 6}
assert data == expected

View File

@ -0,0 +1,94 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2012 New Dream Network, LLC (DreamHost)
#
# Author: Steven Berler <steven.berler@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 getting the sum project volume.
"""
import datetime
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from .base import FunctionalTest
from ceilometer.tests.db import require_map_reduce
class TestSumProjectVolume(FunctionalTest):
PATH = '/projects/project1/meters/volume.size/volume/sum'
def setUp(self):
super(TestSumProjectVolume, self).setUp()
require_map_reduce(self.conn)
self.counters = []
for i in range(3):
c = counter.Counter(
'volume.size',
'gauge',
5 + i,
'user-id',
'project1',
'resource-id-%s' % i,
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
resource_metadata={'display_name': 'test-volume',
'tag': 'self.counter',
}
)
self.counters.append(c)
msg = meter.meter_message_from_counter(c,
cfg.CONF.metering_secret,
'source1',
)
self.conn.record_metering_data(msg)
def test_no_time_bounds(self):
data = self.get_json(self.PATH)
expected = {'volume': 5 + 6 + 7}
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
expected = {'volume': 6 + 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
expected = {'volume': 6}
assert data == expected

View File

@ -0,0 +1,94 @@
# -*- 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 getting the total resource volume.
"""
import datetime
from ceilometer import counter
from ceilometer import meter
from ceilometer.openstack.common import cfg
from .base import FunctionalTest
from ceilometer.tests.db import require_map_reduce
class TestSumResourceVolume(FunctionalTest):
PATH = '/resources/resource-id/meters/volume.size/volume/sum'
def setUp(self):
super(TestSumResourceVolume, self).setUp()
require_map_reduce(self.conn)
self.counters = []
for i in range(3):
c = counter.Counter(
'volume.size',
'gauge',
5 + i,
'user-id',
'project1',
'resource-id',
timestamp=datetime.datetime(2012, 9, 25, 10 + i, 30 + i),
resource_metadata={'display_name': 'test-volume',
'tag': 'self.counter',
}
)
self.counters.append(c)
msg = meter.meter_message_from_counter(c,
cfg.CONF.metering_secret,
'source1',
)
self.conn.record_metering_data(msg)
def test_no_time_bounds(self):
data = self.get_json(self.PATH)
expected = {'volume': 5 + 6 + 7}
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
expected = {'volume': 6 + 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
expected = {'volume': 6}
assert data == expected

View File

@ -15,3 +15,4 @@ python-glanceclient
python-novaclient>=2.6.10 python-novaclient>=2.6.10
python-keystoneclient>=0.2,<0.3 python-keystoneclient>=0.2,<0.3
python-swiftclient python-swiftclient
pecan

View File

@ -14,3 +14,9 @@ https://github.com/dreamhost/Ming/zipball/master#egg=Ming
http://tarballs.openstack.org/nova/nova-master.tar.gz http://tarballs.openstack.org/nova/nova-master.tar.gz
http://tarballs.openstack.org/glance/glance-master.tar.gz http://tarballs.openstack.org/glance/glance-master.tar.gz
setuptools-git>=0.4 setuptools-git>=0.4
# FIXME(dhellmann): We need a version of WSME more current
# than what is released right now. We can't include it in
# pip-requires because we have to point to the Mercurial
# checkout on bitbucket. I hope to have that resolved
# very soon.
hg+https://bitbucket.org/cdevienne/wsme

View File

@ -14,3 +14,9 @@ mox
https://github.com/dreamhost/Ming/zipball/master#egg=Ming https://github.com/dreamhost/Ming/zipball/master#egg=Ming
http://tarballs.openstack.org/glance/glance-stable-folsom.tar.gz http://tarballs.openstack.org/glance/glance-stable-folsom.tar.gz
setuptools-git>=0.4 setuptools-git>=0.4
# FIXME(dhellmann): We need a version of WSME more current
# than what is released right now. We can't include it in
# pip-requires because we have to point to the Mercurial
# checkout on bitbucket. I hope to have that resolved
# very soon.
hg+https://bitbucket.org/cdevienne/wsme