# 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. import copy import functools from neutron_lib import constants import pecan from pecan import request import six from neutron.api import api_common from neutron.api.v2 import attributes as api_attributes from neutron.db import api as db_api from neutron import manager # Utility functions for Pecan controllers. 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) 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): # 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._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')]) 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) 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) def build_field_list(self, request_fields): added_fields = [] combined_fields = [] if request_fields: req_fields_set = set(request_fields) added_fields = self._mandatory_fields - req_fields_set combined_fields = req_fields_set | self._mandatory_fields return list(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 = api_attributes.get_collection_info( 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 six.iteritems(self.resource_info): 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): return getattr(self.plugin, '%s_bulk' % self._plugin_handlers[self.CREATE]) @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 class ShimItemController(NeutronPecanController): def __init__(self, collection, resource, item, controller): super(ShimItemController, self).__init__(collection, resource) self.item = item self.controller_delete = getattr(controller, 'delete', None) @expose(generic=True) def index(self): pecan.abort(405) @when(index, method='DELETE') def delete(self): if not self.controller_delete: pecan.abort(405) pecan.response.status = 204 shim_request = ShimRequest(request.context['neutron_context']) uri_identifiers = request.context['uri_identifiers'] return self.controller_delete(shim_request, self.item, **uri_identifiers) class ShimCollectionsController(NeutronPecanController): def __init__(self, collection, resource, controller): super(ShimCollectionsController, self).__init__(collection, resource) self.controller = controller self.controller_index = getattr(controller, 'index', None) self.controller_create = getattr(controller, 'create', None) @expose(generic=True) def index(self): if not self.controller_index: pecan.abort(405) shim_request = ShimRequest(request.context['neutron_context']) uri_identifiers = request.context['uri_identifiers'] return self.controller_index(shim_request, **uri_identifiers) @when(index, method='POST') def create(self): if not self.controller_create: pecan.abort(405) pecan.response.status = 201 shim_request = ShimRequest(request.context['neutron_context']) uri_identifiers = request.context['uri_identifiers'] return self.controller_create(shim_request, request.json, **uri_identifiers) @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), remainder class PecanResourceExtension(object): def __init__(self, collection, controller, plugin): self.collection = collection self.controller = controller self.plugin = plugin