458 lines
17 KiB
Python
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
|