Remove nova Direct API

blueprint remove-nova-direct-api

Change-Id: I3229f8d7f37d66fcd6b978966f3a428a69e08bb1
This commit is contained in:
Joe Gordon 2012-04-09 14:16:14 -04:00
parent 384b758166
commit 2c14f1818d
6 changed files with 1 additions and 870 deletions

View File

@ -1,110 +0,0 @@
#!/usr/bin/env python
# pylint: disable=C0103
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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.
"""Starter script for Nova Direct API."""
import eventlet
eventlet.monkey_patch()
import os
import sys
# If ../nova/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
from nova import compute
from nova import flags
from nova import log as logging
from nova import network
from nova.openstack.common import cfg
from nova import service
from nova import utils
from nova import volume
from nova import wsgi
from nova.api import direct
direct_api_opts = [
cfg.IntOpt('direct_port',
default=8001,
help='Direct API port'),
cfg.StrOpt('direct_host',
default='0.0.0.0',
help='Direct API host'),
]
FLAGS = flags.FLAGS
FLAGS.register_cli_opts(direct_api_opts)
# An example of an API that only exposes read-only methods.
# In this case we're just limiting which methods are exposed.
class ReadOnlyCompute(direct.Limited):
"""Read-only Compute API."""
_allowed = ['get', 'get_all', 'get_console_output']
# An example of an API that provides a backwards compatibility layer.
# In this case we're overwriting the implementation to ensure
# compatibility with an older version. In reality we would want the
# "description=None" to be part of the actual API so that code
# like this isn't even necessary, but this example shows what one can
# do if that isn't the situation.
class VolumeVersionOne(direct.Limited):
_allowed = ['create', 'delete', 'update', 'get']
def create(self, context, size, name):
self.proxy.create(context, size, name, description=None)
if __name__ == '__main__':
utils.default_flagfile()
FLAGS(sys.argv)
logging.setup()
direct.register_service('compute', compute.API())
direct.register_service('volume', volume.API())
direct.register_service('network', network.API())
direct.register_service('reflect', direct.Reflection())
# Here is how we could expose the code in the examples above.
#direct.register_service('compute-readonly',
# ReadOnlyCompute(compute.API()))
#direct.register_service('volume-v1', VolumeVersionOne(volume.API()))
router = direct.Router()
with_json = direct.JsonParamsMiddleware(router)
with_req = direct.PostParamsMiddleware(with_json)
with_auth = direct.DelegatedAuthMiddleware(with_req)
server = wsgi.Server("Direct API",
with_auth,
host=FLAGS.direct_host,
port=FLAGS.direct_port)
service.serve(server)
service.wait()

162
bin/stack
View File

@ -1,162 +0,0 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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.
"""CLI for the Direct API."""
import eventlet
eventlet.monkey_patch()
import json
import os
import pprint
import sys
import textwrap
import urllib
import urllib2
# If ../nova/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
if os.path.exists(os.path.join(possible_topdir, 'nova', '__init__.py')):
sys.path.insert(0, possible_topdir)
import gflags
FLAGS = gflags.FLAGS
gflags.DEFINE_string('host', '127.0.0.1', 'Direct API host')
gflags.DEFINE_integer('port', 8001, 'Direct API host')
gflags.DEFINE_string('user', 'user1', 'Direct API username')
gflags.DEFINE_string('project', 'proj1', 'Direct API project')
USAGE = """usage: stack [options] <controller> <method> [arg1=value arg2=value]
`stack help` should output the list of available controllers
`stack <controller>` should output the available methods for that controller
`stack help <controller>` should do the same
`stack help <controller> <method>` should output info for a method
"""
def format_help(d):
"""Format help text, keys are labels and values are descriptions."""
MAX_INDENT = 30
indent = max([len(k) for k in d])
if indent > MAX_INDENT:
indent = MAX_INDENT - 6
out = []
for k, v in sorted(d.iteritems()):
if (len(k) + 6) > MAX_INDENT:
out.extend([' %s' % k])
initial_indent = ' ' * (indent + 6)
else:
initial_indent = ' %s ' % k.ljust(indent)
subsequent_indent = ' ' * (indent + 6)
t = textwrap.TextWrapper(initial_indent=initial_indent,
subsequent_indent=subsequent_indent)
out.extend(t.wrap(v))
return out
def help_all():
rv = do_request('reflect', 'get_controllers')
out = format_help(rv)
return (USAGE + str(FLAGS.MainModuleHelp()) +
'\n\nAvailable controllers:\n' +
'\n'.join(out) + '\n')
def help_controller(controller):
rv = do_request('reflect', 'get_methods')
methods = dict([(k.split('/')[2], v) for k, v in rv.iteritems()
if k.startswith('/%s' % controller)])
return ('Available methods for %s:\n' % controller +
'\n'.join(format_help(methods)))
def help_method(controller, method):
rv = do_request('reflect',
'get_method_info',
{'method': '/%s/%s' % (controller, method)})
sig = '%s(%s):' % (method, ', '.join(['='.join(x) for x in rv['args']]))
out = textwrap.wrap(sig, subsequent_indent=' ' * len('%s(' % method))
out.append('\n' + rv['doc'])
return '\n'.join(out)
def do_request(controller, method, params=None):
if params:
data = urllib.urlencode(params)
else:
data = None
url = 'http://%s:%s/%s/%s' % (FLAGS.host, FLAGS.port, controller, method)
headers = {'X-OpenStack-User': FLAGS.user,
'X-OpenStack-Project': FLAGS.project}
req = urllib2.Request(url, data, headers)
try:
resp = urllib2.urlopen(req)
except urllib2.HTTPError, e:
print e.read()
sys.exit(1)
except urllib2.URLError, e:
print 'Failed to connect to %s: %s' % (url, e.reason)
sys.exit(1)
return json.loads(resp.read())
if __name__ == '__main__':
args = FLAGS(sys.argv)
cmd = args.pop(0)
if not args:
print help_all()
sys.exit()
first = args.pop(0)
if first == 'help':
action = help_all
params = []
if args:
params.append(args.pop(0))
action = help_controller
if args:
params.append(args.pop(0))
action = help_method
print action(*params)
sys.exit(0)
controller = first
if not args:
print help_controller(controller)
sys.exit()
method = args.pop(0)
params = {}
for x in args:
key, value = x.split('=', 1)
params[key] = value
pprint.pprint(do_request(controller, method, params))

View File

@ -1,378 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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.
"""Public HTTP interface that allows services to self-register.
The general flow of a request is:
- Request is parsed into WSGI bits.
- Some middleware checks authentication.
- Routing takes place based on the URL to find a controller.
(/controller/method)
- Parameters are parsed from the request and passed to a method on the
controller as keyword arguments.
- Optionally 'json' is decoded to provide all the parameters.
- Actual work is done and a result is returned.
- That result is turned into json and returned.
"""
import inspect
import urllib
import routes
import webob
import nova.api.openstack.wsgi
from nova import context
from nova import exception
from nova import utils
from nova import wsgi
# Global storage for registering modules.
ROUTES = {}
def register_service(path, handle):
"""Register a service handle at a given path.
Services registered in this way will be made available to any instances of
nova.api.direct.Router.
:param path: `routes` path, can be a basic string like "/path"
:param handle: an object whose methods will be made available via the api
"""
ROUTES[path] = handle
class Router(wsgi.Router):
"""A simple WSGI router configured via `register_service`.
This is a quick way to attach multiple services to a given endpoint.
It will automatically load the routes registered in the `ROUTES` global.
TODO(termie): provide a paste-deploy version of this.
"""
def __init__(self, mapper=None):
if mapper is None:
mapper = routes.Mapper()
self._load_registered_routes(mapper)
super(Router, self).__init__(mapper=mapper)
def _load_registered_routes(self, mapper):
for route in ROUTES:
mapper.connect('/%s/{action}' % route,
controller=ServiceWrapper(ROUTES[route]))
class DelegatedAuthMiddleware(wsgi.Middleware):
"""A simple and naive authentication middleware.
Designed mostly to provide basic support for alternative authentication
schemes, this middleware only desires the identity of the user and will
generate the appropriate nova.context.RequestContext for the rest of the
application. This allows any middleware above it in the stack to
authenticate however it would like while only needing to conform to a
minimal interface.
Expects two headers to determine identity:
- X-OpenStack-User
- X-OpenStack-Project
This middleware is tied to identity management and will need to be kept
in sync with any changes to the way identity is dealt with internally.
"""
def process_request(self, request):
os_user = request.headers['X-OpenStack-User']
os_project = request.headers['X-OpenStack-Project']
context_ref = context.RequestContext(user_id=os_user,
project_id=os_project)
request.environ['openstack.context'] = context_ref
class JsonParamsMiddleware(wsgi.Middleware):
"""Middleware to allow method arguments to be passed as serialized JSON.
Accepting arguments as JSON is useful for accepting data that may be more
complex than simple primitives.
In this case we accept it as urlencoded data under the key 'json' as in
json=<urlencoded_json> but this could be extended to accept raw JSON
in the POST body.
Filters out the parameters `self`, `context` and anything beginning with
an underscore.
"""
def process_request(self, request):
if 'json' not in request.params:
return
params_json = request.params['json']
params_parsed = utils.loads(params_json)
params = {}
for k, v in params_parsed.iteritems():
if k in ('self', 'context'):
continue
if k.startswith('_'):
continue
params[k] = v
request.environ['openstack.params'] = params
class PostParamsMiddleware(wsgi.Middleware):
"""Middleware to allow method arguments to be passed as POST parameters.
Filters out the parameters `self`, `context` and anything beginning with
an underscore.
"""
def process_request(self, request):
params_parsed = request.params
params = {}
for k, v in params_parsed.iteritems():
if k in ('self', 'context'):
continue
if k.startswith('_'):
continue
params[k] = v
request.environ['openstack.params'] = params
class Reflection(object):
"""Reflection methods to list available methods.
This is an object that expects to be registered via register_service.
These methods allow the endpoint to be self-describing. They introspect
the exposed methods and provide call signatures and documentation for
them allowing quick experimentation.
"""
def __init__(self):
self._methods = {}
self._controllers = {}
def _gather_methods(self):
"""Introspect available methods and generate documentation for them."""
methods = {}
controllers = {}
for route, handler in ROUTES.iteritems():
controllers[route] = handler.__doc__.split('\n')[0]
for k in dir(handler):
if k.startswith('_'):
continue
f = getattr(handler, k)
if not callable(f):
continue
# bunch of ugly formatting stuff
argspec = inspect.getargspec(f)
args = [x for x in argspec[0]
if x != 'self' and x != 'context']
defaults = argspec[3] and argspec[3] or []
args_r = list(reversed(args))
defaults_r = list(reversed(defaults))
args_out = []
while args_r:
if defaults_r:
args_out.append((args_r.pop(0),
repr(defaults_r.pop(0))))
else:
args_out.append((str(args_r.pop(0)),))
# if the method accepts keywords
if argspec[2]:
args_out.insert(0, ('**%s' % argspec[2],))
if f.__doc__:
short_doc = f.__doc__.split('\n')[0]
doc = f.__doc__
else:
short_doc = doc = _('not available')
methods['/%s/%s' % (route, k)] = {
'short_doc': short_doc,
'doc': doc,
'name': k,
'args': list(reversed(args_out))}
self._methods = methods
self._controllers = controllers
def get_controllers(self, context):
"""List available controllers."""
if not self._controllers:
self._gather_methods()
return self._controllers
def get_methods(self, context):
"""List available methods."""
if not self._methods:
self._gather_methods()
method_list = self._methods.keys()
method_list.sort()
methods = {}
for k in method_list:
methods[k] = self._methods[k]['short_doc']
return methods
def get_method_info(self, context, method):
"""Get detailed information about a method."""
if not self._methods:
self._gather_methods()
return self._methods[method]
class ServiceWrapper(object):
"""Wrapper to dynamically provide a WSGI controller for arbitrary objects.
With lightweight introspection allows public methods on the object to
be accessed via simple WSGI routing and parameters and serializes the
return values.
Automatically used be nova.api.direct.Router to wrap registered instances.
"""
def __init__(self, service_handle):
self.service_handle = service_handle
@webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request)
def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
del arg_dict['action']
context = req.environ['openstack.context']
# allow middleware up the stack to override the params
params = {}
if 'openstack.params' in req.environ:
params = req.environ['openstack.params']
# TODO(termie): do some basic normalization on methods
method = getattr(self.service_handle, action)
# NOTE(vish): make sure we have no unicode keys for py2.6.
params = dict([(str(k), v) for (k, v) in params.iteritems()])
result = method(context, **params)
if result is None or isinstance(result, basestring):
return result
try:
content_type = req.best_match_content_type()
serializer = {
'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(),
'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
}[content_type]
return serializer.serialize(result)
except Exception, e:
raise exception.Error(_("Returned non-serializeable type: %s")
% result)
class Limited(object):
__notdoc = """Limit the available methods on a given object.
(Not a docstring so that the docstring can be conditionally overridden.)
Useful when defining a public API that only exposes a subset of an
internal API.
Expected usage of this class is to define a subclass that lists the allowed
methods in the 'allowed' variable.
Additionally where appropriate methods can be added or overwritten, for
example to provide backwards compatibility.
The wrapping approach has been chosen so that the wrapped API can maintain
its own internal consistency, for example if it calls "self.create" it
should get its own create method rather than anything we do here.
"""
_allowed = None
def __init__(self, proxy):
self._proxy = proxy
if not self.__doc__: # pylint: disable=E0203
self.__doc__ = proxy.__doc__
if not self._allowed:
self._allowed = []
def __getattr__(self, key):
"""Only return methods that are named in self._allowed."""
if key not in self._allowed:
raise AttributeError()
return getattr(self._proxy, key)
def __dir__(self):
"""Only return methods that are named in self._allowed."""
return [x for x in dir(self._proxy) if x in self._allowed]
class Proxy(object):
"""Pretend a Direct API endpoint is an object.
This is mostly useful in testing at the moment though it should be easily
extendable to provide a basic API library functionality.
In testing we use this to stub out internal objects to verify that results
from the API are serializable.
"""
def __init__(self, app, prefix=None):
self.app = app
self.prefix = prefix
def __do_request(self, path, context, **kwargs):
req = wsgi.Request.blank(path)
req.method = 'POST'
req.body = urllib.urlencode({'json': utils.dumps(kwargs)})
req.environ['openstack.context'] = context
resp = req.get_response(self.app)
try:
return utils.loads(resp.body)
except Exception:
return resp.body
def __getattr__(self, key):
if self.prefix is None:
return self.__class__(self.app, prefix=key)
def _wrapper(context, **kwargs):
return self.__do_request('/%s/%s' % (self.prefix, key),
context,
**kwargs)
_wrapper.func_name = key
return _wrapper

View File

@ -1,216 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
# 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.
"""Tests for Direct API."""
import json
import webob
from nova import context
from nova import exception
from nova import test
from nova.api import direct
class ArbitraryObject(object):
pass
class FakeService(object):
def echo(self, context, data):
return {'data': data}
def context(self, context):
return {'user': context.user_id,
'project': context.project_id}
def echo_data_directly(self, context, data):
return data
def invalid_return(self, context):
return ArbitraryObject()
class MyLimited(direct.Limited):
_allowed = ['var1', 'func1']
class MyProxy(object):
var1 = var2 = True
def func1(self):
return True
def func2(self):
return True
class DirectTestCase(test.TestCase):
def setUp(self):
super(DirectTestCase, self).setUp()
direct.register_service('fake', FakeService())
self.router = direct.PostParamsMiddleware(
direct.JsonParamsMiddleware(
direct.Router()))
self.auth_router = direct.DelegatedAuthMiddleware(self.router)
self.context = context.RequestContext('user1', 'proj1')
def tearDown(self):
direct.ROUTES = {}
super(DirectTestCase, self).tearDown()
def test_delegated_auth(self):
req = webob.Request.blank('/fake/context')
req.headers['X-OpenStack-User'] = 'user1'
req.headers['X-OpenStack-Project'] = 'proj1'
resp = req.get_response(self.auth_router)
self.assertEqual(resp.status_int, 200)
data = json.loads(resp.body)
self.assertEqual(data['user'], 'user1')
self.assertEqual(data['project'], 'proj1')
def test_json_params(self):
req = webob.Request.blank('/fake/echo')
req.environ['openstack.context'] = self.context
req.method = 'POST'
req.body = 'json=%s' % json.dumps({'data': 'foo'})
resp = req.get_response(self.router)
self.assertEqual(resp.status_int, 200)
resp_parsed = json.loads(resp.body)
self.assertEqual(resp_parsed['data'], 'foo')
def test_filter_json_params(self):
req = webob.Request.blank('/fake/echo')
req.environ['openstack.context'] = self.context
req.method = 'POST'
req.body = 'json=%s' % json.dumps({'data': 'foo',
'_underscored': 'ignoreMe',
'self': 'ignoreMe',
'context': 'ignoreMe'})
resp = req.get_response(self.router)
self.assertEqual(resp.status_int, 200)
resp_parsed = json.loads(resp.body)
self.assertEqual(resp_parsed['data'], 'foo')
self.assertNotIn('_underscored', resp_parsed)
self.assertNotIn('self', resp_parsed)
self.assertNotIn('context', resp_parsed)
def test_post_params(self):
req = webob.Request.blank('/fake/echo')
req.environ['openstack.context'] = self.context
req.method = 'POST'
req.body = 'data=foo'
resp = req.get_response(self.router)
self.assertEqual(resp.status_int, 200)
resp_parsed = json.loads(resp.body)
self.assertEqual(resp_parsed['data'], 'foo')
def test_filter_post_params(self):
req = webob.Request.blank('/fake/echo')
req.environ['openstack.context'] = self.context
req.method = 'POST'
req.body = ('data=foo&_underscored=ignoreMe&self=ignoreMe&context='
'ignoreMe')
resp = req.get_response(self.router)
self.assertEqual(resp.status_int, 200)
resp_parsed = json.loads(resp.body)
self.assertEqual(resp_parsed['data'], 'foo')
self.assertNotIn('_underscored', resp_parsed)
self.assertNotIn('self', resp_parsed)
self.assertNotIn('context', resp_parsed)
def test_string_resp(self):
req = webob.Request.blank('/fake/echo_data_directly')
req.environ['openstack.context'] = self.context
req.method = 'POST'
req.body = 'data=foo'
resp = req.get_response(self.router)
self.assertEqual(resp.status_int, 200)
self.assertEqual(resp.body, 'foo')
def test_invalid(self):
req = webob.Request.blank('/fake/invalid_return')
req.environ['openstack.context'] = self.context
req.method = 'POST'
self.assertRaises(exception.Error, req.get_response, self.router)
def test_proxy(self):
proxy = direct.Proxy(self.router)
rv = proxy.fake.echo(self.context, data='baz')
self.assertEqual(rv['data'], 'baz')
class LimitedTestCase(test.TestCase):
def test_limited_class_getattr(self):
limited = MyLimited(MyProxy())
# Allowed are still visible
self.assertTrue(limited.func1())
self.assertTrue(limited.var1)
# Non-allowed are no longer visible
self.assertRaises(AttributeError, getattr, limited, 'func2')
self.assertRaises(AttributeError, getattr, limited, 'var2')
def test_limited_class_dir(self):
limited = MyLimited(MyProxy())
# Allowed are still visible
self.assertIn('func1', dir(limited))
self.assertIn('var1', dir(limited))
# Non-allowed are no longer visible
self.assertNotIn('func2', dir(limited))
self.assertNotIn('var2', dir(limited))
def test_limited_class_no_allowed(self):
# New MyLimited class with no _allowed variable
class MyLimited(direct.Limited):
pass
limited = MyLimited(MyProxy())
# Nothing in MyProxy object visible now
self.assertNotIn('func1', dir(limited))
self.assertNotIn('var1', dir(limited))
# NOTE(jkoelker): This fails using the EC2 api
#class DirectCloudTestCase(test_cloud.CloudTestCase):
# def setUp(self):
# super(DirectCloudTestCase, self).setUp()
# compute_handle = compute.API(image_service=self.cloud.image_service)
# volume_handle = volume.API()
# network_handle = network.API()
# direct.register_service('compute', compute_handle)
# direct.register_service('volume', volume_handle)
# direct.register_service('network', network_handle)
#
# self.router = direct.JsonParamsMiddleware(direct.Router())
# proxy = direct.Proxy(self.router)
# self.cloud.compute_api = proxy.compute
# self.cloud.volume_api = proxy.volume
# self.cloud.network_api = proxy.network
# compute_handle.volume_api = proxy.volume
# compute_handle.network_api = proxy.network
#
# def tearDown(self):
# super(DirectCloudTestCase, self).tearDown()
# direct.ROUTES = {}

View File

@ -76,13 +76,11 @@ setuptools.setup(name='nova',
'bin/nova-console',
'bin/nova-consoleauth',
'bin/nova-dhcpbridge',
'bin/nova-direct-api',
'bin/nova-manage',
'bin/nova-network',
'bin/nova-objectstore',
'bin/nova-rootwrap',
'bin/nova-scheduler',
'bin/nova-volume',
'bin/nova-xvpvncproxy',
'bin/stack'],
'bin/nova-xvpvncproxy'],
py_modules=[])

View File

@ -8,7 +8,6 @@ kombu==1.0.4
lockfile==0.8
lxml==2.3
python-daemon==1.5.5
python-gflags==1.3
python-novaclient
routes==1.12.3
WebOb==1.0.8