486 lines
19 KiB
Python
486 lines
19 KiB
Python
# 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.
|
|
"""
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import routes
|
|
import six
|
|
import stevedore
|
|
import webob.dec
|
|
import webob.exc
|
|
|
|
from nova.api.openstack import extensions
|
|
from nova.api.openstack import wsgi
|
|
from nova import exception
|
|
from nova.i18n import _
|
|
from nova.i18n import _LC
|
|
from nova.i18n import _LE
|
|
from nova.i18n import _LI
|
|
from nova.i18n import _LW
|
|
from nova.i18n import translate
|
|
from nova import notifications
|
|
from nova import utils
|
|
from nova import wsgi as base_wsgi
|
|
|
|
|
|
api_opts = [
|
|
cfg.BoolOpt('enabled',
|
|
default=True,
|
|
help='DEPRECATED: Whether the V3 API is enabled or not. '
|
|
'This option will be removed in the 14.0.0 "N" release.',
|
|
deprecated_for_removal=True),
|
|
cfg.ListOpt('extensions_blacklist',
|
|
default=[],
|
|
help='A list of v3 API extensions to never load. '
|
|
'Specify the extension aliases here.'),
|
|
cfg.ListOpt('extensions_whitelist',
|
|
default=[],
|
|
help='If the list is not empty then a v3 API extension '
|
|
'will only be loaded if it exists in this list. Specify '
|
|
'the extension aliases here.')
|
|
]
|
|
api_opts_group = cfg.OptGroup(name='osapi_v3', title='API v3 Options')
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
CONF.register_group(api_opts_group)
|
|
CONF.register_opts(api_opts, api_opts_group)
|
|
|
|
# List of v3 API extensions which are considered to form
|
|
# the core API and so must be present
|
|
# TODO(cyeoh): Expand this list as the core APIs are ported to V3
|
|
API_V3_CORE_EXTENSIONS = set(['os-consoles',
|
|
'extensions',
|
|
'os-flavor-extra-specs',
|
|
'os-flavor-manage',
|
|
'flavors',
|
|
'ips',
|
|
'os-keypairs',
|
|
'os-flavor-access',
|
|
'server-metadata',
|
|
'servers',
|
|
'versions'])
|
|
|
|
|
|
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(_LE("Caught error: %s"), six.text_type(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(_LI("%(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:
|
|
user_locale = req.best_match_language()
|
|
inner_msg = translate(inner.message, user_locale)
|
|
outer.explanation = '%s: %s' % (inner.__class__.__name__,
|
|
inner_msg)
|
|
|
|
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 LegacyV2CompatibleWrapper(base_wsgi.Middleware):
|
|
|
|
def _filter_request_headers(self, req):
|
|
"""For keeping same behavior with v2 API, ignores microversions
|
|
HTTP header X-OpenStack-Nova-API-Version in the request.
|
|
"""
|
|
|
|
if wsgi.API_VERSION_REQUEST_HEADER in req.headers:
|
|
del req.headers[wsgi.API_VERSION_REQUEST_HEADER]
|
|
return req
|
|
|
|
def _filter_response_headers(self, response):
|
|
"""For keeping same behavior with v2 API, filter out microversions
|
|
HTTP header and microversions field in header 'Vary'.
|
|
"""
|
|
|
|
if wsgi.API_VERSION_REQUEST_HEADER in response.headers:
|
|
del response.headers[wsgi.API_VERSION_REQUEST_HEADER]
|
|
|
|
if 'Vary' in response.headers:
|
|
vary_headers = response.headers['Vary'].split(',')
|
|
filtered_vary = []
|
|
for vary in vary_headers:
|
|
vary = vary.strip()
|
|
if vary == wsgi.API_VERSION_REQUEST_HEADER:
|
|
continue
|
|
filtered_vary.append(vary)
|
|
if filtered_vary:
|
|
response.headers['Vary'] = ','.join(filtered_vary)
|
|
else:
|
|
del response.headers['Vary']
|
|
return response
|
|
|
|
@webob.dec.wsgify(RequestClass=wsgi.Request)
|
|
def __call__(self, req):
|
|
req.set_legacy_v2()
|
|
req = self._filter_request_headers(req)
|
|
response = req.get_response(self.application)
|
|
return self._filter_response_headers(response)
|
|
|
|
|
|
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('Extending 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(_LW('Extension %(ext_name)s: Cannot extend '
|
|
'resource %(collection)s: No such resource'),
|
|
msg_format_dict)
|
|
continue
|
|
|
|
LOG.debug('Extension %(ext_name)s extended 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 APIRouterV21(base_wsgi.Router):
|
|
"""Routes requests on the OpenStack v2.1 API to the appropriate controller
|
|
and method.
|
|
"""
|
|
|
|
@classmethod
|
|
def factory(cls, global_config, **local_config):
|
|
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one."""
|
|
return cls()
|
|
|
|
@staticmethod
|
|
def api_extension_namespace():
|
|
# TODO(oomichi): This namespaces will be changed after moving all v3
|
|
# APIs to v2.1.
|
|
return 'nova.api.v3.extensions'
|
|
|
|
def __init__(self, init_only=None, v3mode=False):
|
|
# TODO(cyeoh): bp v3-api-extension-framework. Currently load
|
|
# all extensions but eventually should be able to exclude
|
|
# based on a config file
|
|
# TODO(oomichi): We can remove v3mode argument after moving all v3 APIs
|
|
# to v2.1.
|
|
def _check_load_extension(ext):
|
|
if (self.init_only is None or ext.obj.alias in
|
|
self.init_only) and isinstance(ext.obj,
|
|
extensions.V21APIExtensionBase):
|
|
|
|
# Check whitelist is either empty or if not then the extension
|
|
# is in the whitelist
|
|
if (not CONF.osapi_v3.extensions_whitelist or
|
|
ext.obj.alias in CONF.osapi_v3.extensions_whitelist):
|
|
|
|
# Check the extension is not in the blacklist
|
|
if ext.obj.alias not in CONF.osapi_v3.extensions_blacklist:
|
|
return self._register_extension(ext)
|
|
return False
|
|
|
|
if not CONF.osapi_v3.enabled:
|
|
LOG.info(_LI("V3 API has been disabled by configuration"))
|
|
return
|
|
|
|
self.init_only = init_only
|
|
LOG.debug("v3 API Extension Blacklist: %s",
|
|
CONF.osapi_v3.extensions_blacklist)
|
|
LOG.debug("v3 API Extension Whitelist: %s",
|
|
CONF.osapi_v3.extensions_whitelist)
|
|
|
|
in_blacklist_and_whitelist = set(
|
|
CONF.osapi_v3.extensions_whitelist).intersection(
|
|
CONF.osapi_v3.extensions_blacklist)
|
|
if len(in_blacklist_and_whitelist) != 0:
|
|
LOG.warning(_LW("Extensions in both blacklist and whitelist: %s"),
|
|
list(in_blacklist_and_whitelist))
|
|
|
|
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})
|
|
|
|
if v3mode:
|
|
mapper = PlainMapper()
|
|
else:
|
|
mapper = ProjectMapper()
|
|
|
|
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._register_resources_check_inherits(mapper)
|
|
self.api_extension_manager.map(self._register_controllers)
|
|
|
|
missing_core_extensions = self.get_missing_core_extensions(
|
|
self.loaded_extension_info.get_extensions().keys())
|
|
if not self.init_only and missing_core_extensions:
|
|
LOG.critical(_LC("Missing core API extensions: %s"),
|
|
missing_core_extensions)
|
|
raise exception.CoreAPIMissing(
|
|
missing_apis=missing_core_extensions)
|
|
|
|
LOG.info(_LI("Loaded extensions: %s"),
|
|
sorted(self.loaded_extension_info.get_extensions().keys()))
|
|
super(APIRouterV21, self).__init__(mapper)
|
|
|
|
def _register_resources_list(self, ext_list, mapper):
|
|
for ext in ext_list:
|
|
self._register_resources(ext, mapper)
|
|
|
|
def _register_resources_check_inherits(self, mapper):
|
|
ext_has_inherits = []
|
|
ext_no_inherits = []
|
|
|
|
for ext in self.api_extension_manager:
|
|
for resource in ext.obj.get_resources():
|
|
if resource.inherits:
|
|
ext_has_inherits.append(ext)
|
|
break
|
|
else:
|
|
ext_no_inherits.append(ext)
|
|
|
|
self._register_resources_list(ext_no_inherits, mapper)
|
|
self._register_resources_list(ext_has_inherits, mapper)
|
|
|
|
@staticmethod
|
|
def get_missing_core_extensions(extensions_loaded):
|
|
extensions_loaded = set(extensions_loaded)
|
|
missing_extensions = API_V3_CORE_EXTENSIONS - extensions_loaded
|
|
return list(missing_extensions)
|
|
|
|
@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.ResourceV21(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(_LW('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)
|