2
# vim: tabstop=4 shiftwidth=4 softtabstop=4
4
# Copyright 2011 OpenStack LLC.
5
# Copyright 2011 Justin Santa Barbara
8
# Licensed under the Apache License, Version 2.0 (the "License"); you may
9
# not use this file except in compliance with the License. You may obtain
10
# a copy of the License at
12
# http://www.apache.org/licenses/LICENSE-2.0
14
# Unless required by applicable law or agreed to in writing, software
15
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17
# License for the specific language governing permissions and limitations
20
from abc import ABCMeta
29
from quantum.common import exceptions
30
import quantum.extensions
31
from quantum.manager import QuantumManager
32
from quantum.openstack.common import cfg
33
from quantum.openstack.common import importutils
34
from quantum import wsgi
37
LOG = logging.getLogger('quantum.api.extensions')
39
# Besides the supported_extension_aliases in plugin class,
40
# we also support register enabled extensions here so that we
41
# can load some mandatory files (such as db models) before initialize plugin
43
'quantum.plugins.linuxbridge.lb_quantum_plugin.LinuxBridgePluginV2':
45
'ext_alias': ["quotas"],
46
'ext_db_models': ['quantum.extensions._quotav2_model.Quota'],
48
'quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2':
50
'ext_alias': ["quotas"],
51
'ext_db_models': ['quantum.extensions._quotav2_model.Quota'],
56
class PluginInterface(object):
57
__metaclass__ = ABCMeta
60
def __subclasshook__(cls, klass):
62
The __subclasshook__ method is a class method
63
that will be called everytime a class is tested
64
using issubclass(klass, PluginInterface).
65
In that case, it will check that every method
66
marked with the abstractmethod decorator is
67
provided by the plugin class.
69
for method in cls.__abstractmethods__:
70
if any(method in base.__dict__ for base in klass.__mro__):
76
class ExtensionDescriptor(object):
77
"""Base class that defines the contract for extensions.
79
Note that you don't have to derive from this class to have a valid
80
extension; it is purely a convenience.
85
"""The name of the extension.
90
raise NotImplementedError()
93
"""The alias for the extension.
98
raise NotImplementedError()
100
def get_description(self):
101
"""Friendly description for the extension.
103
e.g. 'The Fox In Socks Extension'
106
raise NotImplementedError()
108
def get_namespace(self):
109
"""The XML namespace for the extension.
111
e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
114
raise NotImplementedError()
116
def get_updated(self):
117
"""The timestamp when the extension was last updated.
119
e.g. '2011-01-22T13:25:27-06:00'
122
# NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
123
raise NotImplementedError()
125
def get_resources(self):
126
"""List of extensions.ResourceExtension extension objects.
128
Resources define new nouns, and are accessible through URLs.
134
def get_actions(self):
135
"""List of extensions.ActionExtension extension objects.
137
Actions are verbs callable from the API.
143
def get_request_extensions(self):
144
"""List of extensions.RequestException extension objects.
146
Request extensions are used to handle custom request data.
152
def get_extended_resources(self, version):
153
"""retrieve extended resources or attributes for core resources.
155
Extended attributes are implemented by a core plugin similarly
156
to the attributes defined in the core, and can appear in
157
request and response messages. Their names are scoped with the
158
extension's prefix. The core API version is passed to this
159
function, which must return a
160
map[<resource_name>][<attribute_name>][<attribute_property>]
161
specifying the extended resource attribute properties required
164
Extension can add resources and their attr definitions too.
165
The returned map can be integrated into RESOURCE_ATTRIBUTE_MAP.
169
def get_plugin_interface(self):
171
Returns an abstract class which defines contract for the plugin.
172
The abstract class should inherit from extesnions.PluginInterface,
173
Methods in this abstract class should be decorated as abstractmethod
178
class ActionExtensionController(wsgi.Controller):
180
def __init__(self, application):
182
self.application = application
183
self.action_handlers = {}
185
def add_action(self, action_name, handler):
186
self.action_handlers[action_name] = handler
188
def action(self, request, id):
190
input_dict = self._deserialize(request.body,
191
request.get_content_type())
192
for action_name, handler in self.action_handlers.iteritems():
193
if action_name in input_dict:
194
return handler(input_dict, request, id)
195
# no action handler found (bump to downstream application)
196
response = self.application
200
class RequestExtensionController(wsgi.Controller):
202
def __init__(self, application):
203
self.application = application
206
def add_handler(self, handler):
207
self.handlers.append(handler)
209
def process(self, request, *args, **kwargs):
210
res = request.get_response(self.application)
211
# currently request handlers are un-ordered
212
for handler in self.handlers:
213
response = handler(request, res)
217
class ExtensionController(wsgi.Controller):
219
def __init__(self, extension_manager):
220
self.extension_manager = extension_manager
222
def _translate(self, ext):
224
ext_data['name'] = ext.get_name()
225
ext_data['alias'] = ext.get_alias()
226
ext_data['description'] = ext.get_description()
227
ext_data['namespace'] = ext.get_namespace()
228
ext_data['updated'] = ext.get_updated()
229
ext_data['links'] = [] # TODO(dprince): implement extension links
232
def index(self, request):
234
for _alias, ext in self.extension_manager.extensions.iteritems():
235
extensions.append(self._translate(ext))
236
return dict(extensions=extensions)
238
def show(self, request, id):
239
# NOTE(dprince): the extensions alias is used as the 'id' for show
240
ext = self.extension_manager.extensions.get(id, None)
242
raise webob.exc.HTTPNotFound(
243
_("Extension with alias %s does not exist") % id)
244
return dict(extension=self._translate(ext))
246
def delete(self, request, id):
247
raise webob.exc.HTTPNotFound()
249
def create(self, request):
250
raise webob.exc.HTTPNotFound()
253
class ExtensionMiddleware(wsgi.Middleware):
254
"""Extensions middleware for WSGI."""
255
def __init__(self, application,
258
self.ext_mgr = (ext_mgr
260
get_extensions_path()))
261
mapper = routes.Mapper()
264
for resource in self.ext_mgr.get_resources():
265
LOG.debug(_('Extended resource: %s'),
267
for action, method in resource.collection_actions.iteritems():
269
parent = resource.parent
270
conditions = dict(method=[method])
271
path = "/%s/%s" % (resource.collection, action)
273
path_prefix = "/%s/{%s_id}" % (parent["collection_name"],
274
parent["member_name"])
275
with mapper.submapper(controller=resource.controller,
277
path_prefix=path_prefix,
278
conditions=conditions) as submap:
280
submap.connect("%s.:(format)" % path)
281
mapper.resource(resource.collection, resource.collection,
282
controller=resource.controller,
283
member=resource.member_actions,
284
parent_resource=resource.parent)
287
action_controllers = self._action_ext_controllers(application,
288
self.ext_mgr, mapper)
289
for action in self.ext_mgr.get_actions():
290
LOG.debug(_('Extended action: %s'), action.action_name)
291
controller = action_controllers[action.collection]
292
controller.add_action(action.action_name, action.handler)
295
req_controllers = self._request_ext_controllers(application,
296
self.ext_mgr, mapper)
297
for request_ext in self.ext_mgr.get_request_extensions():
298
LOG.debug(_('Extended request: %s'), request_ext.key)
299
controller = req_controllers[request_ext.key]
300
controller.add_handler(request_ext.handler)
302
self._router = routes.middleware.RoutesMiddleware(self._dispatch,
304
super(ExtensionMiddleware, self).__init__(application)
307
def factory(cls, global_config, **local_config):
310
return cls(app, global_config, **local_config)
313
def _action_ext_controllers(self, application, ext_mgr, mapper):
314
"""Return a dict of ActionExtensionController-s by collection."""
315
action_controllers = {}
316
for action in ext_mgr.get_actions():
317
if not action.collection in action_controllers.keys():
318
controller = ActionExtensionController(application)
319
mapper.connect("/%s/:(id)/action.:(format)" %
322
controller=controller,
323
conditions=dict(method=['POST']))
324
mapper.connect("/%s/:(id)/action" % action.collection,
326
controller=controller,
327
conditions=dict(method=['POST']))
328
action_controllers[action.collection] = controller
330
return action_controllers
332
def _request_ext_controllers(self, application, ext_mgr, mapper):
333
"""Returns a dict of RequestExtensionController-s by collection."""
334
request_ext_controllers = {}
335
for req_ext in ext_mgr.get_request_extensions():
336
if not req_ext.key in request_ext_controllers.keys():
337
controller = RequestExtensionController(application)
338
mapper.connect(req_ext.url_route + '.:(format)',
340
controller=controller,
341
conditions=req_ext.conditions)
343
mapper.connect(req_ext.url_route,
345
controller=controller,
346
conditions=req_ext.conditions)
347
request_ext_controllers[req_ext.key] = controller
349
return request_ext_controllers
351
@webob.dec.wsgify(RequestClass=wsgi.Request)
352
def __call__(self, req):
353
"""Route the incoming request with router."""
354
req.environ['extended.app'] = self.application
358
@webob.dec.wsgify(RequestClass=wsgi.Request)
360
"""Dispatch the request.
362
Returns the routed WSGI app's response or defers to the extended
366
match = req.environ['wsgiorg.routing_args'][1]
368
return req.environ['extended.app']
369
app = match['controller']
373
def plugin_aware_extension_middleware_factory(global_config, **local_config):
376
ext_mgr = PluginAwareExtensionManager.get_instance()
377
return ExtensionMiddleware(app, ext_mgr=ext_mgr)
381
class ExtensionManager(object):
382
"""Load extensions from the configured extension path.
384
See tests/unit/extensions/foxinsocks.py for an
385
example extension implementation.
388
def __init__(self, path):
389
LOG.info(_('Initializing extension manager.'))
392
self._load_all_extensions()
394
def get_resources(self):
395
"""Returns a list of ResourceExtension objects."""
397
resources.append(ResourceExtension('extensions',
398
ExtensionController(self)))
399
for ext in self.extensions.itervalues():
401
resources.extend(ext.get_resources())
402
except AttributeError:
403
# NOTE(dprince): Extension aren't required to have resource
408
def get_actions(self):
409
"""Returns a list of ActionExtension objects."""
411
for ext in self.extensions.itervalues():
413
actions.extend(ext.get_actions())
414
except AttributeError:
415
# NOTE(dprince): Extension aren't required to have action
420
def get_request_extensions(self):
421
"""Returns a list of RequestExtension objects."""
423
for ext in self.extensions.itervalues():
425
request_exts.extend(ext.get_request_extensions())
426
except AttributeError:
427
# NOTE(dprince): Extension aren't required to have request
432
def extend_resources(self, version, attr_map):
433
"""Extend resources with additional resources or attributes.
435
:param: attr_map, the existing mapping from resource name to
438
After this function, we will extend the attr_map if an extension
439
wants to extend this map.
441
for ext in self.extensions.itervalues():
442
if not hasattr(ext, 'get_extended_resources'):
445
extended_attrs = ext.get_extended_resources(version)
446
for resource, resource_attrs in extended_attrs.iteritems():
447
if attr_map.get(resource, None):
448
attr_map[resource].update(resource_attrs)
450
attr_map[resource] = resource_attrs
451
except AttributeError:
452
LOG.exception("Error fetching extended attributes for "
453
"extension '%s'" % ext.get_name())
455
def _check_extension(self, extension):
456
"""Checks for required methods in extension objects."""
458
LOG.debug(_('Ext name: %s'), extension.get_name())
459
LOG.debug(_('Ext alias: %s'), extension.get_alias())
460
LOG.debug(_('Ext description: %s'), extension.get_description())
461
LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
462
LOG.debug(_('Ext updated: %s'), extension.get_updated())
463
except AttributeError as ex:
464
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
466
if hasattr(extension, 'check_env'):
468
extension.check_env()
469
except exceptions.InvalidExtenstionEnv as ex:
470
LOG.warn(_("Exception loading extension: %s"), unicode(ex))
474
def _load_all_extensions(self):
475
"""Load extensions from the configured path.
477
Load extensions from the configured path. The extension name is
478
constructed from the module_name. If your extension module was named
479
widgets.py the extension class within that module should be
482
See tests/unit/extensions/foxinsocks.py for an example
483
extension implementation.
486
for path in self.path.split(':'):
487
if os.path.exists(path):
488
self._load_all_extensions_from_path(path)
490
LOG.error("Extension path \"%s\" doesn't exist!" % path)
492
def _load_all_extensions_from_path(self, path):
493
for f in os.listdir(path):
495
LOG.info(_('Loading extension file: %s'), f)
496
mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
497
ext_path = os.path.join(path, f)
498
if file_ext.lower() == '.py' and not mod_name.startswith('_'):
499
mod = imp.load_source(mod_name, ext_path)
500
ext_name = mod_name[0].upper() + mod_name[1:]
501
new_ext_class = getattr(mod, ext_name, None)
502
if not new_ext_class:
503
LOG.warn(_('Did not find expected name '
504
'"%(ext_name)s" in %(file)s'),
505
{'ext_name': ext_name,
508
new_ext = new_ext_class()
509
self.add_extension(new_ext)
510
except Exception as exception:
511
LOG.warn("extension file %s wasnt loaded due to %s",
514
def add_extension(self, ext):
515
# Do nothing if the extension doesn't check out
516
if not self._check_extension(ext):
519
alias = ext.get_alias()
520
LOG.warn(_('Loaded extension: %s'), alias)
522
if alias in self.extensions:
523
raise exceptions.Error("Found duplicate extension: %s" %
525
self.extensions[alias] = ext
528
class PluginAwareExtensionManager(ExtensionManager):
532
def __init__(self, path, plugin):
534
super(PluginAwareExtensionManager, self).__init__(path)
536
def _check_extension(self, extension):
537
"""Checks if plugin supports extension and implements the
538
extension contract."""
539
extension_is_valid = super(PluginAwareExtensionManager,
540
self)._check_extension(extension)
541
return (extension_is_valid and
542
self._plugin_supports(extension) and
543
self._plugin_implements_interface(extension))
545
def _plugin_supports(self, extension):
546
alias = extension.get_alias()
547
supports_extension = (hasattr(self.plugin,
548
"supported_extension_aliases") and
549
alias in self.plugin.supported_extension_aliases)
550
plugin_provider = cfg.CONF.core_plugin
551
if not supports_extension and plugin_provider in ENABLED_EXTS:
552
supports_extension = (alias in
553
ENABLED_EXTS[plugin_provider]['ext_alias'])
554
if not supports_extension:
555
LOG.warn("extension %s not supported by plugin %s",
557
return supports_extension
559
def _plugin_implements_interface(self, extension):
560
if(not hasattr(extension, "get_plugin_interface") or
561
extension.get_plugin_interface() is None):
563
plugin_has_interface = isinstance(self.plugin,
564
extension.get_plugin_interface())
565
if not plugin_has_interface:
566
LOG.warn("plugin %s does not implement extension's"
567
"plugin interface %s" % (self.plugin,
568
extension.get_alias()))
569
return plugin_has_interface
572
def get_instance(cls):
573
if cls._instance is None:
574
plugin_provider = cfg.CONF.core_plugin
575
if plugin_provider in ENABLED_EXTS:
576
for model in ENABLED_EXTS[plugin_provider]['ext_db_models']:
577
LOG.debug('loading model %s', model)
578
model_class = importutils.import_class(model)
579
cls._instance = cls(get_extensions_path(),
580
QuantumManager.get_plugin())
584
class RequestExtension(object):
585
"""Extend requests and responses of core Quantum OpenStack API controllers.
587
Provide a way to add data to responses and handle custom request data
588
that is sent to core Quantum OpenStack API controllers.
591
def __init__(self, method, url_route, handler):
592
self.url_route = url_route
593
self.handler = handler
594
self.conditions = dict(method=[method])
595
self.key = "%s-%s" % (method, url_route)
598
class ActionExtension(object):
599
"""Add custom actions to core Quantum OpenStack API controllers."""
601
def __init__(self, collection, action_name, handler):
602
self.collection = collection
603
self.action_name = action_name
604
self.handler = handler
607
class ResourceExtension(object):
608
"""Add top level resources to the OpenStack API in Quantum."""
610
def __init__(self, collection, controller, parent=None,
611
collection_actions={}, member_actions={}):
612
self.collection = collection
613
self.controller = controller
615
self.collection_actions = collection_actions
616
self.member_actions = member_actions
619
# Returns the extention paths from a config entry and the __path__
620
# of quantum.extensions
621
def get_extensions_path():
622
paths = ':'.join(quantum.extensions.__path__)
623
if cfg.CONF.api_extensions_path:
624
paths = ':'.join([cfg.CONF.api_extensions_path, paths])