neutron/neutron/pecan_wsgi/controllers/utils.py

458 lines
17 KiB
Python

# Copyright (c) 2015 Taturiello Consulting, Meh.
# 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.
from collections import defaultdict
import copy
import functools
from neutron_lib.api import attributes
from neutron_lib import constants
from neutron_lib.db import api as db_api
from oslo_log import log as logging
from oslo_utils import excutils
import pecan
from pecan import request
from neutron._i18n import _
from neutron.api import api_common
from neutron import manager
from neutron_lib import exceptions
# Utility functions for Pecan controllers.
LOG = logging.getLogger(__name__)
class Fakecode(object):
co_varnames = ()
def _composed(*decorators):
"""Takes a list of decorators and returns a single decorator."""
def final_decorator(f):
for d in decorators:
# workaround for pecan bug that always assumes decorators
# have a __code__ attr
if not hasattr(d, '__code__'):
setattr(d, '__code__', Fakecode())
f = d(f)
return f
return final_decorator
def _protect_original_resources(f):
"""Wrapper to ensure that mutated resources are discarded on retries."""
@functools.wraps(f)
def wrapped(*args, **kwargs):
ctx = request.context
if 'resources' in ctx:
orig = ctx.get('protected_resources')
if not orig:
# this is the first call so we just take the whole reference
ctx['protected_resources'] = ctx['resources']
# TODO(blogan): Once bug 157751 is fixed and released in
# neutron-lib this memo will no longer be needed. This is just
# quick way to not depend on a release of neutron-lib.
# The version that has that bug fix will need to be updated in
# neutron-lib.
memo = {id(constants.ATTR_NOT_SPECIFIED):
constants.ATTR_NOT_SPECIFIED}
ctx['resources'] = copy.deepcopy(ctx['protected_resources'],
memo=memo)
return f(*args, **kwargs)
return wrapped
def _pecan_generator_wrapper(func, *args, **kwargs):
"""Helper function so we don't have to specify json for everything."""
kwargs.setdefault('content_type', 'application/json')
kwargs.setdefault('template', 'json')
return _composed(_protect_original_resources, db_api.retry_db_errors,
func(*args, **kwargs))
def expose(*args, **kwargs):
return _pecan_generator_wrapper(pecan.expose, *args, **kwargs)
def when(index, *args, **kwargs):
return _pecan_generator_wrapper(index.when, *args, **kwargs)
def when_delete(index, *args, **kwargs):
kwargs['method'] = 'DELETE'
deco = _pecan_generator_wrapper(index.when, *args, **kwargs)
return _composed(_set_del_code, deco)
def _set_del_code(f):
"""Handle logic of disabling json templating engine and setting HTTP code.
We return 204 on delete without content. However, pecan defaults empty
responses with the json template engine to 'null', which is not empty
content. This breaks connection re-use for some clients due to the
inconsistency. So we need to detect when there is no response and
disable the json templating engine.
See https://github.com/pecan/pecan/issues/72
"""
@functools.wraps(f)
def wrapped(*args, **kwargs):
f(*args, **kwargs)
pecan.response.status = 204
pecan.override_template(None)
# NOTE(kevinbenton): we are explicitly not returning the DELETE
# response from the controller because that is the legacy Neutron
# API behavior.
return wrapped
class NeutronPecanController(object):
LIST = 'list'
SHOW = 'show'
CREATE = 'create'
UPDATE = 'update'
DELETE = 'delete'
def __init__(self, collection, resource, plugin=None, resource_info=None,
allow_pagination=None, allow_sorting=None,
parent_resource=None, member_actions=None,
collection_actions=None, item=None, action_status=None):
# Ensure dashes are always replaced with underscores
self.collection = collection and collection.replace('-', '_')
self.resource = resource and resource.replace('-', '_')
self._member_actions = member_actions or {}
self._collection_actions = collection_actions or {}
self._resource_info = resource_info
self._plugin = plugin
# Controllers for some resources that are not mapped to anything in
# RESOURCE_ATTRIBUTE_MAP will not have anything in _resource_info
if self.resource_info:
self._mandatory_fields = set([field for (field, data) in
self.resource_info.items() if
data.get('required_by_policy')])
if 'tenant_id' in self._mandatory_fields:
# ensure that project_id is queried in the database when
# tenant_id is required
self._mandatory_fields.add('project_id')
else:
self._mandatory_fields = set()
self.allow_pagination = allow_pagination
if self.allow_pagination is None:
self.allow_pagination = True
self.allow_sorting = allow_sorting
if self.allow_sorting is None:
self.allow_sorting = True
self.native_pagination = api_common.is_native_pagination_supported(
self.plugin)
self.native_sorting = api_common.is_native_sorting_supported(
self.plugin)
if self.allow_pagination and self.native_pagination:
if not self.native_sorting:
raise exceptions.Invalid(
_("Native pagination depends on native sorting")
)
self.filter_validation = api_common.is_filter_validation_supported(
self.plugin)
self.primary_key = self._get_primary_key()
self.parent = parent_resource
parent_resource = '_%s' % parent_resource if parent_resource else ''
self._parent_id_name = ('%s_id' % self.parent
if self.parent else None)
self._plugin_handlers = {
self.LIST: 'get%s_%s' % (parent_resource, self.collection),
self.SHOW: 'get%s_%s' % (parent_resource, self.resource)
}
for action in [self.CREATE, self.UPDATE, self.DELETE]:
self._plugin_handlers[action] = '%s%s_%s' % (
action, parent_resource, self.resource)
self.item = item
self.action_status = action_status or {}
def _set_response_code(self, result, method_name):
if method_name in self.action_status:
pecan.response.status = self.action_status[method_name]
else:
pecan.response.status = 200 if result else 204
def build_field_list(self, request_fields):
added_fields = []
combined_fields = []
req_fields_set = {f for f in request_fields if f}
if req_fields_set:
added_fields = self._mandatory_fields - req_fields_set
combined_fields = req_fields_set | self._mandatory_fields
# field sorting is to match old behavior of legacy API and to make
# this drop-in compatible with the old API unit tests
return sorted(combined_fields), list(added_fields)
@property
def plugin(self):
if not self._plugin:
self._plugin = manager.NeutronManager.get_plugin_for_resource(
self.collection)
return self._plugin
@property
def resource_info(self):
if not self._resource_info:
self._resource_info = attributes.RESOURCES.get(
self.collection)
return self._resource_info
def _get_primary_key(self, default_primary_key='id'):
if not self.resource_info:
return default_primary_key
for key, value in self.resource_info.items():
if value.get('primary_key', False):
return key
return default_primary_key
@property
def plugin_handlers(self):
return self._plugin_handlers
@property
def plugin_lister(self):
return getattr(self.plugin, self._plugin_handlers[self.LIST])
@property
def plugin_shower(self):
return getattr(self.plugin, self._plugin_handlers[self.SHOW])
@property
def plugin_creator(self):
return getattr(self.plugin, self._plugin_handlers[self.CREATE])
@property
def plugin_bulk_creator(self):
native = getattr(self.plugin,
'%s_bulk' % self._plugin_handlers[self.CREATE],
None)
# NOTE(kevinbenton): this flag is just to make testing easier since we
# don't have any in-tree plugins without native bulk support
if getattr(self.plugin, '_FORCE_EMULATED_BULK', False) or not native:
return self._emulated_bulk_creator
return native
def _emulated_bulk_creator(self, context, **kwargs):
objs = []
body = kwargs[self.collection]
try:
for item in body[self.collection]:
objs.append(self.plugin_creator(context, item))
return objs
except Exception:
with excutils.save_and_reraise_exception():
for obj in objs:
try:
self.plugin_deleter(context, obj['id'])
except Exception:
LOG.exception("Unable to undo bulk create for "
"%(resource)s %(id)s",
{'resource': self.collection,
'id': obj['id']})
@property
def plugin_deleter(self):
return getattr(self.plugin, self._plugin_handlers[self.DELETE])
@property
def plugin_updater(self):
return getattr(self.plugin, self._plugin_handlers[self.UPDATE])
class ShimRequest(object):
def __init__(self, context):
self.context = context
def invert_dict(dictionary):
inverted = defaultdict(list)
for k, v in dictionary.items():
inverted[v].append(k)
return inverted
class ShimItemController(NeutronPecanController):
def __init__(self, collection, resource, item, controller,
collection_actions=None, member_actions=None,
action_status=None):
super(ShimItemController, self).__init__(
collection, resource, collection_actions=collection_actions,
member_actions=member_actions, item=item,
action_status=action_status)
self.controller = controller
self.controller_delete = getattr(controller, 'delete', None)
self.controller_update = getattr(controller, 'update', None)
self.controller_show = getattr(controller, 'show', None)
self.inverted_collection_actions = invert_dict(
self._collection_actions)
@expose(generic=True)
def index(self):
shim_request = ShimRequest(request.context['neutron_context'])
kwargs = request.context['uri_identifiers']
if self.item in self.inverted_collection_actions['GET']:
method = getattr(self.controller, self.item, None)
# collection actions should not take an self.item because they are
# essentially static items.
result = method(shim_request, **kwargs)
self._set_response_code(result, self.item)
return result
elif not self.controller_show:
pecan.abort(405)
else:
result = self.controller_show(shim_request, self.item, **kwargs)
self._set_response_code(result, 'show')
return result
@when_delete(index)
def delete(self):
if not self.controller_delete:
pecan.abort(405)
shim_request = ShimRequest(request.context['neutron_context'])
uri_identifiers = request.context['uri_identifiers']
result = self.controller_delete(shim_request, self.item,
**uri_identifiers)
self._set_response_code(result, 'delete')
return result
@when(index, method='PUT')
def update(self):
if not self.controller_update:
pecan.abort(405)
pecan.response.status = self.action_status.get('update', 201)
shim_request = ShimRequest(request.context['neutron_context'])
kwargs = request.context['uri_identifiers']
try:
kwargs['body'] = request.context['request_data']
except KeyError:
pass
result = self.controller_update(shim_request, self.item,
**kwargs)
self._set_response_code(result, 'update')
return result
@expose()
def _lookup(self, resource, *remainder):
request.context['resource'] = self.resource
return ShimMemberActionController(self.collection, resource, self.item,
self.controller,
self._member_actions), remainder
class ShimCollectionsController(NeutronPecanController):
def __init__(self, collection, resource, controller,
collection_actions=None, member_actions=None,
collection_methods=None, action_status=None):
collection_methods = collection_methods or {}
super(ShimCollectionsController, self).__init__(
collection, resource, member_actions=member_actions,
collection_actions=collection_actions,
action_status=action_status)
self.controller = controller
self.controller_index = getattr(controller, 'index', None)
self.controller_create = getattr(controller, 'create', None)
self.controller_update = getattr(controller, 'update', None)
self.collection_methods = {}
for action, method in collection_methods.items():
controller_method = getattr(controller, action, None)
self.collection_methods[method] = (
controller_method, self.action_status.get(action, 200))
@expose(generic=True)
def index(self):
if (not self.controller_index and
request.method not in self.collection_methods):
pecan.abort(405)
controller_method_status = self.collection_methods.get(request.method)
status = None
if controller_method_status:
controller_method = controller_method_status[0]
status = controller_method_status[1]
else:
controller_method = self.controller_index
shim_request = ShimRequest(request.context['neutron_context'])
uri_identifiers = request.context['uri_identifiers']
args = [shim_request]
if request.method == 'PUT':
args.append(request.context.get('request_data'))
result = controller_method(*args, **uri_identifiers)
if not status:
self._set_response_code(result, 'index')
else:
pecan.response.status = status
return result
@when(index, method='POST')
def create(self):
if not self.controller_create:
pecan.abort(405)
shim_request = ShimRequest(request.context['neutron_context'])
uri_identifiers = request.context['uri_identifiers']
result = self.controller_create(shim_request,
request.context.get('request_data'),
**uri_identifiers)
self._set_response_code(result, 'create')
return result
@expose()
def _lookup(self, item, *remainder):
request.context['resource'] = self.resource
request.context['resource_id'] = item
return (
ShimItemController(self.collection, self.resource, item,
self.controller,
member_actions=self._member_actions,
collection_actions=self._collection_actions,
action_status=self.action_status),
remainder
)
class ShimMemberActionController(NeutronPecanController):
def __init__(self, collection, resource, item, controller,
member_actions):
super(ShimMemberActionController, self).__init__(
collection, resource, member_actions=member_actions, item=item)
self.controller = controller
self.inverted_member_actions = invert_dict(self._member_actions)
@expose(generic=True)
def index(self):
if self.resource not in self.inverted_member_actions['GET']:
pecan.abort(404)
shim_request = ShimRequest(request.context['neutron_context'])
uri_identifiers = request.context['uri_identifiers']
method = getattr(self.controller, self.resource)
return method(shim_request, self.item, **uri_identifiers)
class PecanResourceExtension(object):
def __init__(self, collection, controller, plugin):
self.collection = collection
self.controller = controller
self.plugin = plugin