1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
7
# Licensed under the Apache License, Version 2.0 (the "License"); you may
8
# not use this file except in compliance with the License. You may obtain
9
# a copy of the License at
11
# http://www.apache.org/licenses/LICENSE-2.0
13
# Unless required by applicable law or agreed to in writing, software
14
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
# License for the specific language governing permissions and limitations
19
"""Public HTTP interface that allows services to self-register.
21
The general flow of a request is:
22
- Request is parsed into WSGI bits.
23
- Some middleware checks authentication.
24
- Routing takes place based on the URL to find a controller.
26
- Parameters are parsed from the request and passed to a method on the
27
controller as keyword arguments.
28
- Optionally 'json' is decoded to provide all the parameters.
29
- Actual work is done and a result is returned.
30
- That result is turned into json and returned.
40
from nova import context
41
from nova import exception
42
from nova import flags
43
from nova import utils
45
import nova.api.openstack.wsgi
48
# Global storage for registering modules.
52
def register_service(path, handle):
53
"""Register a service handle at a given path.
55
Services registered in this way will be made available to any instances of
56
nova.api.direct.Router.
58
:param path: `routes` path, can be a basic string like "/path"
59
:param handle: an object whose methods will be made available via the api
65
class Router(wsgi.Router):
66
"""A simple WSGI router configured via `register_service`.
68
This is a quick way to attach multiple services to a given endpoint.
69
It will automatically load the routes registered in the `ROUTES` global.
71
TODO(termie): provide a paste-deploy version of this.
75
def __init__(self, mapper=None):
77
mapper = routes.Mapper()
79
self._load_registered_routes(mapper)
80
super(Router, self).__init__(mapper=mapper)
82
def _load_registered_routes(self, mapper):
84
mapper.connect('/%s/{action}' % route,
85
controller=ServiceWrapper(ROUTES[route]))
88
class DelegatedAuthMiddleware(wsgi.Middleware):
89
"""A simple and naive authentication middleware.
91
Designed mostly to provide basic support for alternative authentication
92
schemes, this middleware only desires the identity of the user and will
93
generate the appropriate nova.context.RequestContext for the rest of the
94
application. This allows any middleware above it in the stack to
95
authenticate however it would like while only needing to conform to a
98
Expects two headers to determine identity:
100
- X-OpenStack-Project
102
This middleware is tied to identity management and will need to be kept
103
in sync with any changes to the way identity is dealt with internally.
107
def process_request(self, request):
108
os_user = request.headers['X-OpenStack-User']
109
os_project = request.headers['X-OpenStack-Project']
110
context_ref = context.RequestContext(user=os_user, project=os_project)
111
request.environ['openstack.context'] = context_ref
114
class JsonParamsMiddleware(wsgi.Middleware):
115
"""Middleware to allow method arguments to be passed as serialized JSON.
117
Accepting arguments as JSON is useful for accepting data that may be more
118
complex than simple primitives.
120
In this case we accept it as urlencoded data under the key 'json' as in
121
json=<urlencoded_json> but this could be extended to accept raw JSON
124
Filters out the parameters `self`, `context` and anything beginning with
129
def process_request(self, request):
130
if 'json' not in request.params:
133
params_json = request.params['json']
134
params_parsed = utils.loads(params_json)
136
for k, v in params_parsed.iteritems():
137
if k in ('self', 'context'):
139
if k.startswith('_'):
143
request.environ['openstack.params'] = params
146
class PostParamsMiddleware(wsgi.Middleware):
147
"""Middleware to allow method arguments to be passed as POST parameters.
149
Filters out the parameters `self`, `context` and anything beginning with
154
def process_request(self, request):
155
params_parsed = request.params
157
for k, v in params_parsed.iteritems():
158
if k in ('self', 'context'):
160
if k.startswith('_'):
164
request.environ['openstack.params'] = params
167
class Reflection(object):
168
"""Reflection methods to list available methods.
170
This is an object that expects to be registered via register_service.
171
These methods allow the endpoint to be self-describing. They introspect
172
the exposed methods and provide call signatures and documentation for
173
them allowing quick experimentation.
179
self._controllers = {}
181
def _gather_methods(self):
182
"""Introspect available methods and generate documentation for them."""
185
for route, handler in ROUTES.iteritems():
186
controllers[route] = handler.__doc__.split('\n')[0]
187
for k in dir(handler):
188
if k.startswith('_'):
190
f = getattr(handler, k)
194
# bunch of ugly formatting stuff
195
argspec = inspect.getargspec(f)
196
args = [x for x in argspec[0]
197
if x != 'self' and x != 'context']
198
defaults = argspec[3] and argspec[3] or []
199
args_r = list(reversed(args))
200
defaults_r = list(reversed(defaults))
205
args_out.append((args_r.pop(0),
206
repr(defaults_r.pop(0))))
208
args_out.append((str(args_r.pop(0)),))
210
# if the method accepts keywords
212
args_out.insert(0, ('**%s' % argspec[2],))
215
short_doc = f.__doc__.split('\n')[0]
218
short_doc = doc = _('not available')
220
methods['/%s/%s' % (route, k)] = {
221
'short_doc': short_doc,
224
'args': list(reversed(args_out))}
226
self._methods = methods
227
self._controllers = controllers
229
def get_controllers(self, context):
230
"""List available controllers."""
231
if not self._controllers:
232
self._gather_methods()
234
return self._controllers
236
def get_methods(self, context):
237
"""List available methods."""
238
if not self._methods:
239
self._gather_methods()
241
method_list = self._methods.keys()
244
for k in method_list:
245
methods[k] = self._methods[k]['short_doc']
248
def get_method_info(self, context, method):
249
"""Get detailed information about a method."""
250
if not self._methods:
251
self._gather_methods()
252
return self._methods[method]
255
class ServiceWrapper(object):
256
"""Wrapper to dynamically povide a WSGI controller for arbitrary objects.
258
With lightweight introspection allows public methods on the object to
259
be accesed via simple WSGI routing and parameters and serializes the
262
Automatically used be nova.api.direct.Router to wrap registered instances.
266
def __init__(self, service_handle):
267
self.service_handle = service_handle
269
@webob.dec.wsgify(RequestClass=nova.api.openstack.wsgi.Request)
270
def __call__(self, req):
271
arg_dict = req.environ['wsgiorg.routing_args'][1]
272
action = arg_dict['action']
273
del arg_dict['action']
275
context = req.environ['openstack.context']
276
# allow middleware up the stack to override the params
278
if 'openstack.params' in req.environ:
279
params = req.environ['openstack.params']
281
# TODO(termie): do some basic normalization on methods
282
method = getattr(self.service_handle, action)
284
# NOTE(vish): make sure we have no unicode keys for py2.6.
285
params = dict([(str(k), v) for (k, v) in params.iteritems()])
286
result = method(context, **params)
288
if result is None or type(result) is str or type(result) is unicode:
292
content_type = req.best_match_content_type()
294
'application/xml': nova.api.openstack.wsgi.XMLDictSerializer(),
295
'application/json': nova.api.openstack.wsgi.JSONDictSerializer(),
297
return serializer.serialize(result)
299
raise exception.Error("returned non-serializable type: %s"
303
class Limited(object):
304
__notdoc = """Limit the available methods on a given object.
306
(Not a docstring so that the docstring can be conditionally overriden.)
308
Useful when defining a public API that only exposes a subset of an
311
Expected usage of this class is to define a subclass that lists the allowed
312
methods in the 'allowed' variable.
314
Additionally where appropriate methods can be added or overwritten, for
315
example to provide backwards compatibility.
317
The wrapping approach has been chosen so that the wrapped API can maintain
318
its own internal consistency, for example if it calls "self.create" it
319
should get its own create method rather than anything we do here.
325
def __init__(self, proxy):
327
if not self.__doc__: # pylint: disable=E0203
328
self.__doc__ = proxy.__doc__
329
if not self._allowed:
332
def __getattr__(self, key):
333
"""Only return methods that are named in self._allowed."""
334
if key not in self._allowed:
335
raise AttributeError()
336
return getattr(self._proxy, key)
339
"""Only return methods that are named in self._allowed."""
340
return [x for x in dir(self._proxy) if x in self._allowed]
344
"""Pretend a Direct API endpoint is an object.
346
This is mostly useful in testing at the moment though it should be easily
347
extendable to provide a basic API library functionality.
349
In testing we use this to stub out internal objects to verify that results
350
from the API are serializable.
354
def __init__(self, app, prefix=None):
358
def __do_request(self, path, context, **kwargs):
359
req = wsgi.Request.blank(path)
361
req.body = urllib.urlencode({'json': utils.dumps(kwargs)})
362
req.environ['openstack.context'] = context
363
resp = req.get_response(self.app)
365
return utils.loads(resp.body)
369
def __getattr__(self, key):
370
if self.prefix is None:
371
return self.__class__(self.app, prefix=key)
373
def _wrapper(context, **kwargs):
374
return self.__do_request('/%s/%s' % (self.prefix, key),
377
_wrapper.func_name = key