d7da449eef
This is the second patch for the new extension framework
which is only to be used by the Nova v3 API.
- Adds tracking of extensions loaded and allows
extensions access to this information
- Adds core API functionality as extensions
- 'server'
- Adds an entry point that other extensions can
use to modify the server create arguments without
having to modify the server extension itself
- TODO: Will have to add more entry points as other
extensions are ported. Delaying adding entry points
now so they can be tested as they are added.
- Adds port of os-keypairs extension
- This is an example of a controller extension in the new
framework
- This is an example of using the server extension entry
point to add functionality without modify the core API code
- Ports tests for the os-keypairs extensions
- Adds v3 API fake specific code for tests
This completes the bulk of the new extension framework. Porting
of the server tests will be done in future changesets as more
of the core API is ported across as the tests are dependent
on multiple core APIs.
Partially implements blueprint v3-api-extension-framework
Change-Id: Ibadb5bbe808c27d2f4afebe65c06a92576397085
335 lines
13 KiB
Python
335 lines
13 KiB
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.
|
|
|
|
"""
|
|
WSGI middleware for OpenStack API controllers.
|
|
"""
|
|
|
|
import routes
|
|
import stevedore
|
|
import webob.dec
|
|
import webob.exc
|
|
|
|
from nova.api.openstack import extensions
|
|
from nova.api.openstack import wsgi
|
|
from nova import notifications
|
|
from nova.openstack.common import log as logging
|
|
from nova import utils
|
|
from nova import wsgi as base_wsgi
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class FaultWrapper(base_wsgi.Middleware):
|
|
"""Calls down the middleware stack, making exceptions into faults."""
|
|
|
|
_status_to_type = {}
|
|
|
|
@staticmethod
|
|
def status_to_type(status):
|
|
if not FaultWrapper._status_to_type:
|
|
for clazz in utils.walk_class_hierarchy(webob.exc.HTTPError):
|
|
FaultWrapper._status_to_type[clazz.code] = clazz
|
|
return FaultWrapper._status_to_type.get(
|
|
status, webob.exc.HTTPInternalServerError)()
|
|
|
|
def _error(self, inner, req):
|
|
LOG.exception(_("Caught error: %s"), unicode(inner))
|
|
|
|
safe = getattr(inner, 'safe', False)
|
|
headers = getattr(inner, 'headers', None)
|
|
status = getattr(inner, 'code', 500)
|
|
if status is None:
|
|
status = 500
|
|
|
|
msg_dict = dict(url=req.url, status=status)
|
|
LOG.info(_("%(url)s returned with HTTP %(status)d") % msg_dict)
|
|
outer = self.status_to_type(status)
|
|
if headers:
|
|
outer.headers = headers
|
|
# NOTE(johannes): We leave the explanation empty here on
|
|
# purpose. It could possibly have sensitive information
|
|
# that should not be returned back to the user. See
|
|
# bugs 868360 and 874472
|
|
# NOTE(eglynn): However, it would be over-conservative and
|
|
# inconsistent with the EC2 API to hide every exception,
|
|
# including those that are safe to expose, see bug 1021373
|
|
if safe:
|
|
outer.explanation = '%s: %s' % (inner.__class__.__name__,
|
|
unicode(inner))
|
|
|
|
notifications.send_api_fault(req.url, status, inner)
|
|
return wsgi.Fault(outer)
|
|
|
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
|
def __call__(self, req):
|
|
try:
|
|
return req.get_response(self.application)
|
|
except Exception as ex:
|
|
return self._error(ex, req)
|
|
|
|
|
|
class APIMapper(routes.Mapper):
|
|
def routematch(self, url=None, environ=None):
|
|
if url == "":
|
|
result = self._match("", environ)
|
|
return result[0], result[1]
|
|
return routes.Mapper.routematch(self, url, environ)
|
|
|
|
def connect(self, *args, **kargs):
|
|
# NOTE(vish): Default the format part of a route to only accept json
|
|
# and xml so it doesn't eat all characters after a '.'
|
|
# in the url.
|
|
kargs.setdefault('requirements', {})
|
|
if not kargs['requirements'].get('format'):
|
|
kargs['requirements']['format'] = 'json|xml'
|
|
return routes.Mapper.connect(self, *args, **kargs)
|
|
|
|
|
|
class ProjectMapper(APIMapper):
|
|
def resource(self, member_name, collection_name, **kwargs):
|
|
if 'parent_resource' not in kwargs:
|
|
kwargs['path_prefix'] = '{project_id}/'
|
|
else:
|
|
parent_resource = kwargs['parent_resource']
|
|
p_collection = parent_resource['collection_name']
|
|
p_member = parent_resource['member_name']
|
|
kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection,
|
|
p_member)
|
|
routes.Mapper.resource(self, member_name,
|
|
collection_name,
|
|
**kwargs)
|
|
|
|
|
|
class PlainMapper(APIMapper):
|
|
def resource(self, member_name, collection_name, **kwargs):
|
|
if 'parent_resource' in kwargs:
|
|
parent_resource = kwargs['parent_resource']
|
|
p_collection = parent_resource['collection_name']
|
|
p_member = parent_resource['member_name']
|
|
kwargs['path_prefix'] = '%s/:%s_id' % (p_collection, p_member)
|
|
routes.Mapper.resource(self, member_name,
|
|
collection_name,
|
|
**kwargs)
|
|
|
|
|
|
class APIRouter(base_wsgi.Router):
|
|
"""
|
|
Routes requests on the OpenStack API to the appropriate controller
|
|
and method.
|
|
"""
|
|
ExtensionManager = None # override in subclasses
|
|
|
|
@classmethod
|
|
def factory(cls, global_config, **local_config):
|
|
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
|
|
return cls()
|
|
|
|
def __init__(self, ext_mgr=None, init_only=None):
|
|
if ext_mgr is None:
|
|
if self.ExtensionManager:
|
|
ext_mgr = self.ExtensionManager()
|
|
else:
|
|
raise Exception(_("Must specify an ExtensionManager class"))
|
|
|
|
mapper = ProjectMapper()
|
|
self.resources = {}
|
|
self._setup_routes(mapper, ext_mgr, init_only)
|
|
self._setup_ext_routes(mapper, ext_mgr, init_only)
|
|
self._setup_extensions(ext_mgr)
|
|
super(APIRouter, self).__init__(mapper)
|
|
|
|
def _setup_ext_routes(self, mapper, ext_mgr, init_only):
|
|
for resource in ext_mgr.get_resources():
|
|
LOG.debug(_('Extended resource: %s'),
|
|
resource.collection)
|
|
|
|
if init_only is not None and resource.collection not in init_only:
|
|
continue
|
|
|
|
inherits = None
|
|
if resource.inherits:
|
|
inherits = self.resources.get(resource.inherits)
|
|
if not resource.controller:
|
|
resource.controller = inherits.controller
|
|
wsgi_resource = wsgi.Resource(resource.controller,
|
|
inherits=inherits)
|
|
self.resources[resource.collection] = wsgi_resource
|
|
kargs = dict(
|
|
controller=wsgi_resource,
|
|
collection=resource.collection_actions,
|
|
member=resource.member_actions)
|
|
|
|
if resource.parent:
|
|
kargs['parent_resource'] = resource.parent
|
|
|
|
mapper.resource(resource.collection, resource.collection, **kargs)
|
|
|
|
if resource.custom_routes_fn:
|
|
resource.custom_routes_fn(mapper, wsgi_resource)
|
|
|
|
def _setup_extensions(self, ext_mgr):
|
|
for extension in ext_mgr.get_controller_extensions():
|
|
collection = extension.collection
|
|
controller = extension.controller
|
|
|
|
msg_format_dict = {'collection': collection,
|
|
'ext_name': extension.extension.name}
|
|
if collection not in self.resources:
|
|
LOG.warning(_('Extension %(ext_name)s: Cannot extend '
|
|
'resource %(collection)s: No such resource'),
|
|
msg_format_dict)
|
|
continue
|
|
|
|
LOG.debug(_('Extension %(ext_name)s extending resource: '
|
|
'%(collection)s'),
|
|
msg_format_dict)
|
|
|
|
resource = self.resources[collection]
|
|
resource.register_actions(controller)
|
|
resource.register_extensions(controller)
|
|
|
|
def _setup_routes(self, mapper, ext_mgr, init_only):
|
|
raise NotImplementedError()
|
|
|
|
|
|
class APIRouterV3(base_wsgi.Router):
|
|
"""
|
|
Routes requests on the OpenStack v3 API to the appropriate controller
|
|
and method.
|
|
"""
|
|
|
|
API_EXTENSION_NAMESPACE = 'nova.api.v3.extensions'
|
|
|
|
@classmethod
|
|
def factory(cls, global_config, **local_config):
|
|
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
|
|
return cls()
|
|
|
|
def __init__(self, init_only=None):
|
|
# TODO(cyeoh): bp v3-api-extension-framework. Currently load
|
|
# all extensions but eventually should be able to exclude
|
|
# based on a config file
|
|
def _check_load_extension(ext):
|
|
if (self.init_only is None or ext.obj.alias in
|
|
self.init_only) and isinstance(ext.obj,
|
|
extensions.V3APIExtensionBase):
|
|
return self._register_extension(ext)
|
|
else:
|
|
return False
|
|
|
|
self.init_only = init_only
|
|
self.api_extension_manager = stevedore.enabled.EnabledExtensionManager(
|
|
namespace=self.API_EXTENSION_NAMESPACE,
|
|
check_func=_check_load_extension,
|
|
invoke_on_load=True,
|
|
invoke_kwds={"extension_info": self.loaded_extension_info})
|
|
|
|
mapper = PlainMapper()
|
|
self.resources = {}
|
|
|
|
# NOTE(cyeoh) Core API support is rewritten as extensions
|
|
# but conceptually still have core
|
|
if list(self.api_extension_manager):
|
|
# NOTE(cyeoh): Stevedore raises an exception if there are
|
|
# no plugins detected. I wonder if this is a bug.
|
|
self.api_extension_manager.map(self._register_resources,
|
|
mapper=mapper)
|
|
self.api_extension_manager.map(self._register_controllers)
|
|
|
|
super(APIRouterV3, self).__init__(mapper)
|
|
|
|
@property
|
|
def loaded_extension_info(self):
|
|
raise NotImplementedError
|
|
|
|
def _register_extension(self, ext):
|
|
raise NotImplementedError()
|
|
|
|
def _register_resources(self, ext, mapper):
|
|
"""Register resources defined by the extensions
|
|
|
|
Extensions define what resources they want to add through a
|
|
get_resources function
|
|
"""
|
|
|
|
handler = ext.obj
|
|
LOG.debug("Running _register_resources on %s", ext.obj)
|
|
|
|
for resource in handler.get_resources():
|
|
LOG.debug(_('Extended resource: %s'), resource.collection)
|
|
|
|
inherits = None
|
|
if resource.inherits:
|
|
inherits = self.resources.get(resource.inherits)
|
|
if not resource.controller:
|
|
resource.controller = inherits.controller
|
|
wsgi_resource = wsgi.Resource(resource.controller,
|
|
inherits=inherits)
|
|
self.resources[resource.collection] = wsgi_resource
|
|
kargs = dict(
|
|
controller=wsgi_resource,
|
|
collection=resource.collection_actions,
|
|
member=resource.member_actions)
|
|
|
|
if resource.parent:
|
|
kargs['parent_resource'] = resource.parent
|
|
|
|
# non core-API plugins use the collection name as the
|
|
# member name, but the core-API plugins use the
|
|
# singular/plural convention for member/collection names
|
|
if resource.member_name:
|
|
member_name = resource.member_name
|
|
else:
|
|
member_name = resource.collection
|
|
mapper.resource(member_name, resource.collection,
|
|
**kargs)
|
|
|
|
if resource.custom_routes_fn:
|
|
resource.custom_routes_fn(mapper, wsgi_resource)
|
|
|
|
def _register_controllers(self, ext):
|
|
"""Register controllers defined by the extensions
|
|
|
|
Extensions define what resources they want to add through
|
|
a get_controller_extensions function
|
|
"""
|
|
|
|
handler = ext.obj
|
|
LOG.debug("Running _register_controllers on %s", ext.obj)
|
|
|
|
for extension in handler.get_controller_extensions():
|
|
ext_name = extension.extension.name
|
|
collection = extension.collection
|
|
controller = extension.controller
|
|
|
|
if collection not in self.resources:
|
|
LOG.warning(_('Extension %(ext_name)s: Cannot extend '
|
|
'resource %(collection)s: No such resource'),
|
|
{'ext_name': ext_name, 'collection': collection})
|
|
continue
|
|
|
|
LOG.debug(_('Extension %(ext_name)s extending resource: '
|
|
'%(collection)s'),
|
|
{'ext_name': ext_name, 'collection': collection})
|
|
|
|
resource = self.resources[collection]
|
|
resource.register_actions(controller)
|
|
resource.register_extensions(controller)
|