16
16
# License for the specific language governing permissions and limitations
17
17
# under the License.
26
from melange.common import exception
27
from melange.common import wsgi
30
LOG = logging.getLogger('melange.common.extensions')
33
class ExtensionDescriptor(object):
34
"""Base class that defines the contract for extensions.
36
Note that you don't have to derive from this class to have a valid
37
extension; it is purely a convenience.
42
"""The name of the extension.
47
raise NotImplementedError()
50
"""The alias for the extension.
55
raise NotImplementedError()
57
def get_description(self):
58
"""Friendly description for the extension.
60
e.g. 'The Fox In Socks Extension'
63
raise NotImplementedError()
65
def get_namespace(self):
66
"""The XML namespace for the extension.
68
e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
71
raise NotImplementedError()
73
def get_updated(self):
74
"""The timestamp when the extension was last updated.
76
e.g. '2011-01-22T13:25:27-06:00'
79
# NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
80
raise NotImplementedError()
82
def get_resources(self):
83
"""List of extensions.ResourceExtension extension objects.
85
Resources define new nouns, and are accessible through URLs.
91
def get_actions(self):
92
"""List of extensions.ActionExtension extension objects.
94
Actions are verbs callable from the API.
100
def get_request_extensions(self):
101
"""List of extensions.RequestException extension objects.
103
Request extensions are used to handle custom request data.
110
class ActionExtensionController(wsgi.Controller):
112
def __init__(self, application):
114
self.application = application
115
self.action_handlers = {}
116
super(ActionExtensionController, self).__init__()
118
def add_action(self, action_name, handler):
119
self.action_handlers[action_name] = handler
121
def action(self, request, id, body=None):
122
for action_name, handler in self.action_handlers.iteritems():
123
if action_name in body:
124
return handler(body, request, id)
125
# no action handler found (bump to downstream application)
126
response = self.application
130
class RequestExtensionController(wsgi.Controller):
132
def __init__(self, application):
133
self.application = application
135
super(RequestExtensionController, self).__init__()
137
def add_handler(self, handler):
138
self.handlers.append(handler)
140
def process(self, request, *args, **kwargs):
141
res = request.get_response(self.application)
142
# currently request handlers are un-ordered
143
for handler in self.handlers:
144
res = handler(request, res)
148
class ExtensionController(wsgi.Controller):
150
def __init__(self, extension_manager):
151
self.extension_manager = extension_manager
152
super(ExtensionController, self).__init__()
154
def _translate(self, ext):
156
ext_data['name'] = ext.get_name()
157
ext_data['alias'] = ext.get_alias()
158
ext_data['description'] = ext.get_description()
159
ext_data['namespace'] = ext.get_namespace()
160
ext_data['updated'] = ext.get_updated()
161
ext_data['links'] = [] # TODO(dprince): implement extension links
164
def index(self, request):
166
for _alias, ext in self.extension_manager.extensions.iteritems():
167
extensions.append(self._translate(ext))
168
return dict(extensions=extensions)
170
def show(self, request, id):
171
# NOTE(dprince): the extensions alias is used as the 'id' for show
172
ext = self.extension_manager.extensions[id]
173
return dict(extension=self._translate(ext))
175
def delete(self, request, id):
176
raise webob.exc.HTTPNotFound()
178
def create(self, request):
179
raise webob.exc.HTTPNotFound()
182
class ExtensionMiddleware(wsgi.Middleware):
183
"""Extensions middleware for WSGI."""
186
def factory(cls, global_config, **local_config):
189
return cls(app, global_config, **local_config)
192
def _action_ext_controllers(self, application, ext_mgr, mapper):
193
"""Return a dict of ActionExtensionController-s by collection."""
194
action_controllers = {}
195
for action in ext_mgr.get_actions():
196
if not action.collection in action_controllers.keys():
197
controller = ActionExtensionController(application)
198
mapper.connect("/%s/:(id)/action.:(format)" %
201
controller=controller.create_resource(),
202
conditions=dict(method=['POST']))
203
mapper.connect("/%s/:(id)/action" % action.collection,
205
controller=controller.create_resource(),
206
conditions=dict(method=['POST']))
207
action_controllers[action.collection] = controller
209
return action_controllers
211
def _request_ext_controllers(self, application, ext_mgr, mapper):
212
"""Returns a dict of RequestExtensionController-s by collection."""
213
request_ext_controllers = {}
214
for req_ext in ext_mgr.get_request_extensions():
215
if not req_ext.key in request_ext_controllers.keys():
216
controller = RequestExtensionController(application)
217
mapper.connect(req_ext.url_route + '.:(format)',
219
controller=controller.create_resource(),
220
conditions=req_ext.conditions)
222
mapper.connect(req_ext.url_route,
224
controller=controller.create_resource(),
225
conditions=req_ext.conditions)
226
request_ext_controllers[req_ext.key] = controller
228
return request_ext_controllers
230
def __init__(self, application, config_params,
232
self.ext_mgr = (ext_mgr
233
or ExtensionManager(config_params.get('api_extensions_path',
236
mapper = routes.Mapper()
239
for resource in self.ext_mgr.get_resources():
240
LOG.debug(_('Extended resource: %s'),
242
mapper.resource(resource.collection, resource.collection,
243
controller=resource.controller.create_resource(),
244
collection=resource.collection_actions,
245
member=resource.member_actions,
246
parent_resource=resource.parent)
249
action_controllers = self._action_ext_controllers(application,
250
self.ext_mgr, mapper)
251
for action in self.ext_mgr.get_actions():
252
LOG.debug(_('Extended action: %s'), action.action_name)
253
controller = action_controllers[action.collection]
254
controller.add_action(action.action_name, action.handler)
257
req_controllers = self._request_ext_controllers(application,
258
self.ext_mgr, mapper)
259
for request_ext in self.ext_mgr.get_request_extensions():
260
LOG.debug(_('Extended request: %s'), request_ext.key)
261
controller = req_controllers[request_ext.key]
262
controller.add_handler(request_ext.handler)
264
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
267
super(ExtensionMiddleware, self).__init__(application)
269
@webob.dec.wsgify(RequestClass=wsgi.Request)
270
def __call__(self, request):
271
"""Route the incoming request with router."""
272
request.environ['extended.app'] = self.application
276
@webob.dec.wsgify(RequestClass=wsgi.Request)
277
def _dispatch(request):
278
"""Dispatch the request.
280
Returns the routed WSGI app's response or defers to the extended
284
match = request.environ['wsgiorg.routing_args'][1]
286
return request.environ['extended.app']
287
app = match['controller']
291
class ExtensionManager(object):
292
"""Load extensions from the configured extension path.
294
See melange/tests/unit/extensions/foxinsocks.py for an
295
example extension implementation.
299
def __init__(self, path):
300
LOG.info(_('Initializing extension manager.'))
304
self._load_all_extensions()
306
def get_resources(self):
307
"""Returns a list of ResourceExtension objects."""
309
resources.append(ResourceExtension('extensions',
310
ExtensionController(self)))
311
for alias, ext in self.extensions.iteritems():
313
resources.extend(ext.get_resources())
314
except AttributeError:
315
# NOTE(dprince): Extension aren't required to have resource
320
def get_actions(self):
321
"""Returns a list of ActionExtension objects."""
323
for alias, ext in self.extensions.iteritems():
325
actions.extend(ext.get_actions())
326
except AttributeError:
327
# NOTE(dprince): Extension aren't required to have action
332
def get_request_extensions(self):
333
"""Returns a list of RequestExtension objects."""
335
for alias, ext in self.extensions.iteritems():
337
request_exts.extend(ext.get_request_extensions())
338
except AttributeError:
339
# NOTE(dprince): Extension aren't required to have request
344
def _check_extension(self, extension):
345
"""Checks for required methods in extension objects."""
347
LOG.debug(_('Ext name: %s'), extension.get_name())
348
LOG.debug(_('Ext alias: %s'), extension.get_alias())
349
LOG.debug(_('Ext description: %s'), extension.get_description())
350
LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
351
LOG.debug(_('Ext updated: %s'), extension.get_updated())
352
except AttributeError as ex:
353
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
355
def _load_all_extensions(self):
356
"""Load extensions from the configured path.
358
Load extensions from the configured path. The extension name is
359
constructed from the module_name. If your extension module was named
360
widgets.py the extension class within that module should be
363
In addition, extensions are loaded from the 'contrib' directory.
365
See melange/tests/unit/extensions/foxinsocks.py for an example
366
extension implementation.
369
if os.path.exists(self.path):
370
self._load_all_extensions_from_path(self.path)
372
contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
373
if os.path.exists(contrib_path):
374
self._load_all_extensions_from_path(contrib_path)
376
def _load_all_extensions_from_path(self, path):
377
for f in os.listdir(path):
378
LOG.info(_('Loading extension file: %s'), f)
379
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
380
ext_path = os.path.join(path, f)
381
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
382
mod = imp.load_source(mod_name, ext_path)
383
ext_name = mod_name[0].upper() + mod_name[1:]
384
new_ext_class = getattr(mod, ext_name, None)
385
if not new_ext_class:
386
LOG.warn(_('Did not find expected name '
387
'"%(ext_name)s" in %(file)s'),
388
{'ext_name': ext_name,
391
new_ext = new_ext_class()
392
self._check_extension(new_ext)
393
self._add_extension(new_ext)
395
def _add_extension(self, ext):
396
alias = ext.get_alias()
397
LOG.info(_('Loaded extension: %s'), alias)
399
self._check_extension(ext)
401
if alias in self.extensions:
402
raise exception.MelangeError(_("Found duplicate extension: %s")
404
self.extensions[alias] = ext
407
class RequestExtension(object):
408
"""Extend requests and responses of core melange OpenStack API controllers.
410
Provide a way to add data to responses and handle custom request data
411
that is sent to core melange OpenStack API controllers.
414
def __init__(self, method, url_route, handler):
415
self.url_route = url_route
416
self.handler = handler
417
self.conditions = dict(method=[method])
418
self.key = "%s-%s" % (method, url_route)
421
class ActionExtension(object):
422
"""Add custom actions to core melange OpenStack API controllers."""
424
def __init__(self, collection, action_name, handler):
425
self.collection = collection
426
self.action_name = action_name
427
self.handler = handler
430
class ResourceExtension(object):
431
"""Add top level resources to the OpenStack API in melange."""
433
def __init__(self, collection, controller, parent=None,
434
collection_actions=None, member_actions=None,
435
deserializer=None, serializer=None):
436
self.collection = collection
437
self.controller = controller
439
self.collection_actions = collection_actions or {}
440
self.member_actions = member_actions or {}
441
self.deserializer = deserializer
442
self.serializer = serializer
19
from openstack.common import extensions
22
def factory(global_config, **local_config):
25
extensions.DEFAULT_XMLNS = "http://docs.openstack.org/melange"
26
ext_mgr = extensions.ExtensionManager(
27
global_config.get('api_extensions_path', ''))
28
return extensions.ExtensionMiddleware(app, global_config, ext_mgr)