From 874ba55a4933e917bca1756fab16d45eb5abf4c0 Mon Sep 17 00:00:00 2001 From: He Jie Xu Date: Tue, 28 Mar 2017 23:19:47 +0800 Subject: [PATCH] Use plain routes list for '/servers' endpoint instead of stevedore This patch add '/servers' related routes by a plain list, instead of using stevedore. After all the Nova API endpoints moves to the plain routes list, the usage of stevedore for loading the API will be removed from Nova. To remove the servers extension from stevedore, all the extensions which depend on servers needs to be removed together. Those extensions are about the servers API response extension and the action extension. Also note that the original 'ProjectMapper' use the 'routes.Mapper.resource' to create a set of routes for a resource which comform to the Atom publishing protocol. It includes some of URL mappings we didn't document before. This patch will remove those URL mappings, also remove the corresponding URL mappings for 'os-volumes_boot' endpoint. For the detail, please reference: http://lists.openstack.org/pipermail/openstack-dev/2017-March/114736.html Partial-implement-blueprint api-no-more-extensions-pike Change-Id: I76c384c10bd804fc2049aef305044149bb55d0dc --- nova/api/openstack/__init__.py | 21 ++- nova/api/openstack/compute/__init__.py | 27 +-- nova/api/openstack/compute/extension_info.py | 62 ++++++ nova/api/openstack/compute/floating_ips.py | 4 +- nova/api/openstack/compute/remote_consoles.py | 4 +- nova/api/openstack/compute/routes.py | 177 ++++++++++++++++++ nova/api/openstack/compute/security_groups.py | 6 +- nova/api/openstack/compute/volumes.py | 4 - .../api/openstack/compute/test_extensions.py | 21 --- setup.cfg | 22 --- 10 files changed, 266 insertions(+), 82 deletions(-) create mode 100644 nova/api/openstack/compute/routes.py diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index 0626d98e98..93aaf6d976 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -152,7 +152,7 @@ class APIMapper(routes.Mapper): class ProjectMapper(APIMapper): - def resource(self, member_name, collection_name, **kwargs): + def _get_project_id_token(self): # NOTE(sdague): project_id parameter is only valid if its hex # or hex + dashes (note, integers are a subset of this). This # is required to hand our overlaping routes issues. @@ -160,7 +160,10 @@ class ProjectMapper(APIMapper): if CONF.osapi_v21.project_id_regex: project_id_regex = CONF.osapi_v21.project_id_regex - project_id_token = '{project_id:%s}' % project_id_regex + return '{project_id:%s}' % project_id_regex + + def resource(self, member_name, collection_name, **kwargs): + project_id_token = self._get_project_id_token() if 'parent_resource' not in kwargs: kwargs['path_prefix'] = '%s/' % project_id_token else: @@ -191,6 +194,20 @@ class ProjectMapper(APIMapper): collection_name, **kwargs) + def create_route(self, path, method, controller, action): + project_id_token = self._get_project_id_token() + + # while we transition away from project IDs in the API URIs, create + # additional routes that include the project_id + self.connect('/%s%s' % (project_id_token, path), + conditions=dict(method=[method]), + controller=controller, + action=action) + self.connect(path, + conditions=dict(method=[method]), + controller=controller, + action=action) + class PlainMapper(APIMapper): def resource(self, member_name, collection_name, **kwargs): diff --git a/nova/api/openstack/compute/__init__.py b/nova/api/openstack/compute/__init__.py index afea72442c..b8e155554c 100644 --- a/nova/api/openstack/compute/__init__.py +++ b/nova/api/openstack/compute/__init__.py @@ -14,25 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. -""" -WSGI middleware for OpenStack Compute API. -""" - -import nova.api.openstack -from nova.api.openstack.compute import extension_info - - -class APIRouterV21(nova.api.openstack.APIRouterV21): - """Routes requests on the OpenStack API to the appropriate controller - and method. - """ - def __init__(self): - self._loaded_extension_info = extension_info.LoadedExtensionInfo() - super(APIRouterV21, self).__init__() - - def _register_extension(self, ext): - return self.loaded_extension_info.register_extension(ext.obj) - - @property - def loaded_extension_info(self): - return self._loaded_extension_info +# The APIRouterV21 moves down to the 'nova.api.openstack.compute.routes' for +# circle reference problem. Import the APIRouterV21 is for the api-paste.ini +# works correctly without modification. We still looking for a chance to move +# the APIRouterV21 back to here after cleanups. +from nova.api.openstack.compute.routes import APIRouterV21 # noqa diff --git a/nova/api/openstack/compute/extension_info.py b/nova/api/openstack/compute/extension_info.py index fd4800c3f8..750b177b4c 100644 --- a/nova/api/openstack/compute/extension_info.py +++ b/nova/api/openstack/compute/extension_info.py @@ -18,6 +18,28 @@ from oslo_log import log as logging import webob.exc +from nova.api.openstack.compute import admin_actions +from nova.api.openstack.compute import admin_password +from nova.api.openstack.compute import config_drive +from nova.api.openstack.compute import console_output +from nova.api.openstack.compute import create_backup +from nova.api.openstack.compute import deferred_delete +from nova.api.openstack.compute import evacuate +from nova.api.openstack.compute import extended_availability_zone +from nova.api.openstack.compute import extended_server_attributes +from nova.api.openstack.compute import extended_status +from nova.api.openstack.compute import extended_volumes +from nova.api.openstack.compute import hide_server_addresses +from nova.api.openstack.compute import lock_server +from nova.api.openstack.compute import migrate_server +from nova.api.openstack.compute import multinic +from nova.api.openstack.compute import pause_server +from nova.api.openstack.compute import rescue +from nova.api.openstack.compute import scheduler_hints +from nova.api.openstack.compute import server_usage +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import shelve +from nova.api.openstack.compute import suspend_server from nova.api.openstack import extensions from nova.api.openstack import wsgi from nova import exception @@ -163,6 +185,36 @@ hardcoded_extensions = [ 'alias': 'os-personality'}, ] + +# TODO(alex_xu): This is a list of unused extension objs. Add those +# extension objs here for building a compatible extension API. Finally, +# we should remove those extension objs, and add corresponding entries +# in the 'hardcoded_extensions'. +unused_extension_objs = [ + admin_actions.AdminActions, + admin_password.AdminPassword, + config_drive.ConfigDrive, + console_output.ConsoleOutput, + create_backup.CreateBackup, + deferred_delete.DeferredDelete, + evacuate.Evacuate, + extended_availability_zone.ExtendedAvailabilityZone, + extended_server_attributes.ExtendedServerAttributes, + extended_status.ExtendedStatus, + extended_volumes.ExtendedVolumes, + hide_server_addresses.HideServerAddresses, + lock_server.LockServer, + migrate_server.MigrateServer, + multinic.Multinic, + pause_server.PauseServer, + rescue.Rescue, + scheduler_hints.SchedulerHints, + server_usage.ServerUsage, + servers.Servers, + shelve.Shelve, + suspend_server.SuspendServer +] + # V2.1 does not support XML but we need to keep an entry in the # /extensions information returned to the user for backwards # compatibility @@ -219,6 +271,16 @@ class ExtensionInfoController(wsgi.Controller): item['description'] ) + for ext_cls in unused_extension_objs: + ext = ext_cls(None) + action = ':'.join([ + base_policies.COMPUTE_API, ext.alias, 'discoverable']) + if context.can(action, fatal=False): + discoverable_extensions[ext.alias] = ext + else: + LOG.debug("Filter out extension %s from discover list", + ext.alias) + for alias, ext in self.extension_info.get_extensions().items(): action = ':'.join([ base_policies.COMPUTE_API, alias, 'discoverable']) diff --git a/nova/api/openstack/compute/floating_ips.py b/nova/api/openstack/compute/floating_ips.py index ac472e72c5..c8e5d184f2 100644 --- a/nova/api/openstack/compute/floating_ips.py +++ b/nova/api/openstack/compute/floating_ips.py @@ -338,6 +338,4 @@ class FloatingIps(extensions.V21APIExtensionBase): return resource def get_controller_extensions(self): - controller = FloatingIPActionController() - extension = extensions.ControllerExtension(self, 'servers', controller) - return [extension] + return [] diff --git a/nova/api/openstack/compute/remote_consoles.py b/nova/api/openstack/compute/remote_consoles.py index ff4a71775f..bf0ff67803 100644 --- a/nova/api/openstack/compute/remote_consoles.py +++ b/nova/api/openstack/compute/remote_consoles.py @@ -195,9 +195,7 @@ class RemoteConsoles(extensions.V21APIExtensionBase): version = 1 def get_controller_extensions(self): - controller = RemoteConsolesController() - extension = extensions.ControllerExtension(self, 'servers', controller) - return [extension] + return [] def get_resources(self): parent = {'member_name': 'server', diff --git a/nova/api/openstack/compute/routes.py b/nova/api/openstack/compute/routes.py new file mode 100644 index 0000000000..d3a4de7b02 --- /dev/null +++ b/nova/api/openstack/compute/routes.py @@ -0,0 +1,177 @@ +# 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. + +import functools + +import nova.api.openstack +from nova.api.openstack.compute import admin_actions +from nova.api.openstack.compute import admin_password +from nova.api.openstack.compute import config_drive +from nova.api.openstack.compute import console_output +from nova.api.openstack.compute import create_backup +from nova.api.openstack.compute import deferred_delete +from nova.api.openstack.compute import evacuate +from nova.api.openstack.compute import extended_availability_zone +from nova.api.openstack.compute import extended_server_attributes +from nova.api.openstack.compute import extended_status +from nova.api.openstack.compute import extended_volumes +from nova.api.openstack.compute import extension_info +from nova.api.openstack.compute import floating_ips +from nova.api.openstack.compute import hide_server_addresses +from nova.api.openstack.compute import keypairs +from nova.api.openstack.compute import lock_server +from nova.api.openstack.compute import migrate_server +from nova.api.openstack.compute import multinic +from nova.api.openstack.compute import pause_server +from nova.api.openstack.compute import remote_consoles +from nova.api.openstack.compute import rescue +from nova.api.openstack.compute import security_groups +from nova.api.openstack.compute import server_usage +from nova.api.openstack.compute import servers +from nova.api.openstack.compute import shelve +from nova.api.openstack.compute import suspend_server +from nova.api.openstack import wsgi +import nova.conf + + +CONF = nova.conf.CONF + + +def _create_controller(main_controller, controller_list, + action_controller_list): + """This is a helper method to create controller with a + list of extended controller. This is for backward compatible + with old extension interface. Finally, the controller for the + same resource will be merged into single one controller. + """ + + controller = wsgi.ResourceV21(main_controller()) + for ctl in controller_list: + controller.register_extensions(ctl()) + for ctl in action_controller_list: + controller.register_actions(ctl()) + return controller + + +server_controller = functools.partial(_create_controller, + servers.ServersController, + [ + config_drive.ConfigDriveController, + extended_availability_zone.ExtendedAZController, + extended_server_attributes.ExtendedServerAttributesController, + extended_status.ExtendedStatusController, + extended_volumes.ExtendedVolumesController, + hide_server_addresses.Controller, + keypairs.Controller, + security_groups.SecurityGroupsOutputController, + server_usage.ServerUsageController, + ], + [ + admin_actions.AdminActionsController, + admin_password.AdminPasswordController, + console_output.ConsoleOutputController, + create_backup.CreateBackupController, + deferred_delete.DeferredDeleteController, + evacuate.EvacuateController, + floating_ips.FloatingIPActionController, + lock_server.LockServerController, + migrate_server.MigrateServerController, + multinic.MultinicController, + pause_server.PauseServerController, + remote_consoles.RemoteConsolesController, + rescue.RescueController, + security_groups.SecurityGroupActionController, + shelve.ShelveController, + suspend_server.SuspendServerController + ] +) + + +# NOTE(alex_xu): This is structure of this route list as below: +# ( +# ('Route path': { +# 'HTTP method: [ +# 'Controller', +# 'The method of controller is used to handle this route' +# ], +# ... +# }), +# ... +# ) +# +# Also note that this is ordered tuple. For example, the '/servers/detail' +# should be in the front of '/servers/{id}', otherwise the request to +# '/servers/detail' always matches to '/servers/{id}' as the id is 'detail'. +ROUTE_LIST = ( + # NOTE: '/os-volumes_boot' is a clone of '/servers'. We may want to + # deprecate it in the future. + ('/os-volumes_boot', { + 'GET': [server_controller, 'index'], + 'POST': [server_controller, 'create'] + }), + ('/os-volumes_boot/detail', { + 'GET': [server_controller, 'detail'] + }), + ('/os-volumes_boot/{id}', { + 'GET': [server_controller, 'show'], + 'PUT': [server_controller, 'update'], + 'DELETE': [server_controller, 'delete'] + }), + ('/os-volumes_boot/{id}/action', { + 'POST': [server_controller, 'action'] + }), + ('/servers', { + 'GET': [server_controller, 'index'], + 'POST': [server_controller, 'create'] + }), + ('/servers/detail', { + 'GET': [server_controller, 'detail'] + }), + ('/servers/{id}', { + 'GET': [server_controller, 'show'], + 'PUT': [server_controller, 'update'], + 'DELETE': [server_controller, 'delete'] + }), + ('/servers/{id}/action', { + 'POST': [server_controller, 'action'] + }) +) + + +class APIRouterV21(nova.api.openstack.APIRouterV21): + """Routes requests on the OpenStack API to the appropriate controller + and method. The URL mapping based on the plain list `ROUTE_LIST` is built + at here. The stevedore based API loading will be replaced by this. + """ + def __init__(self): + self._loaded_extension_info = extension_info.LoadedExtensionInfo() + super(APIRouterV21, self).__init__() + + for path, methods in ROUTE_LIST: + for method, controller_info in methods.items(): + # TODO(alex_xu): In the end, I want to create single controller + # instance instead of create controller instance for each + # route. + controller = controller_info[0]() + action = controller_info[1] + self.map.create_route(path, method, controller, action) + + def _register_extension(self, ext): + return self.loaded_extension_info.register_extension(ext.obj) + + @property + def loaded_extension_info(self): + return self._loaded_extension_info diff --git a/nova/api/openstack/compute/security_groups.py b/nova/api/openstack/compute/security_groups.py index 3ed786139f..8e1d2e0d59 100644 --- a/nova/api/openstack/compute/security_groups.py +++ b/nova/api/openstack/compute/security_groups.py @@ -503,11 +503,7 @@ class SecurityGroups(extensions.V21APIExtensionBase): version = 1 def get_controller_extensions(self): - secgrp_output_ext = extensions.ControllerExtension( - self, 'servers', SecurityGroupsOutputController()) - secgrp_act_ext = extensions.ControllerExtension( - self, 'servers', SecurityGroupActionController()) - return [secgrp_output_ext, secgrp_act_ext] + return [] def get_resources(self): secgrp_ext = extensions.ResourceExtension(ALIAS, diff --git a/nova/api/openstack/compute/volumes.py b/nova/api/openstack/compute/volumes.py index 406b4933f8..e5ea504220 100644 --- a/nova/api/openstack/compute/volumes.py +++ b/nova/api/openstack/compute/volumes.py @@ -608,10 +608,6 @@ class Volumes(extensions.V21APIExtensionBase): ALIAS, VolumeController(), collection_actions={'detail': 'GET'}) resources.append(res) - res = extensions.ResourceExtension('os-volumes_boot', - inherits='servers') - resources.append(res) - res = extensions.ResourceExtension('os-volume_attachments', VolumeAttachmentController(), parent=dict( diff --git a/nova/tests/unit/api/openstack/compute/test_extensions.py b/nova/tests/unit/api/openstack/compute/test_extensions.py index 07136059f1..6d06b45b87 100644 --- a/nova/tests/unit/api/openstack/compute/test_extensions.py +++ b/nova/tests/unit/api/openstack/compute/test_extensions.py @@ -13,10 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import webob.exc -from nova.api.openstack import compute from nova.api.openstack.compute import extension_info from nova.api.openstack import extensions from nova import exception @@ -30,29 +28,10 @@ class fake_bad_extension(object): class ExtensionLoadingTestCase(test.NoDBTestCase): - def test_extensions_loaded(self): - app = compute.APIRouterV21() - self.assertIn('servers', app._loaded_extension_info.extensions) - def test_check_bad_extension(self): loaded_ext_info = extension_info.LoadedExtensionInfo() self.assertFalse(loaded_ext_info._check_extension(fake_bad_extension)) - @mock.patch('nova.api.openstack.APIRouterV21._register_resources_list') - def test_extensions_inherit(self, mock_register): - app = compute.APIRouterV21() - self.assertIn('servers', app._loaded_extension_info.extensions) - self.assertIn('os-volumes', app._loaded_extension_info.extensions) - - mock_register.assert_called_with(mock.ANY, mock.ANY) - ext_no_inherits = mock_register.call_args_list[0][0][0] - ext_has_inherits = mock_register.call_args_list[1][0][0] - # os-volumes inherits from servers - name_list = [ext.obj.alias for ext in ext_has_inherits] - self.assertIn('os-volumes', name_list) - name_list = [ext.obj.alias for ext in ext_no_inherits] - self.assertIn('servers', name_list) - def test_extensions_expected_error(self): @extensions.expected_errors(404) def fake_func(): diff --git a/setup.cfg b/setup.cfg index ee57afd104..6c3ce4ad00 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,8 +71,6 @@ wsgi_scripts = nova-placement-api = nova.api.openstack.placement.wsgi:init_application nova.api.v21.extensions = - admin_actions = nova.api.openstack.compute.admin_actions:AdminActions - admin_password = nova.api.openstack.compute.admin_password:AdminPassword agents = nova.api.openstack.compute.agents:Agents aggregates = nova.api.openstack.compute.aggregates:Aggregates assisted_volume_snapshots = nova.api.openstack.compute.assisted_volume_snapshots:AssistedVolumeSnapshots @@ -83,17 +81,8 @@ nova.api.v21.extensions = cells = nova.api.openstack.compute.cells:Cells certificates = nova.api.openstack.compute.certificates:Certificates cloudpipe = nova.api.openstack.compute.cloudpipe:Cloudpipe - config_drive = nova.api.openstack.compute.config_drive:ConfigDrive console_auth_tokens = nova.api.openstack.compute.console_auth_tokens:ConsoleAuthTokens - console_output = nova.api.openstack.compute.console_output:ConsoleOutput consoles = nova.api.openstack.compute.consoles:Consoles - create_backup = nova.api.openstack.compute.create_backup:CreateBackup - deferred_delete = nova.api.openstack.compute.deferred_delete:DeferredDelete - evacuate = nova.api.openstack.compute.evacuate:Evacuate - extended_availability_zone = nova.api.openstack.compute.extended_availability_zone:ExtendedAvailabilityZone - extended_server_attributes = nova.api.openstack.compute.extended_server_attributes:ExtendedServerAttributes - extended_status = nova.api.openstack.compute.extended_status:ExtendedStatus - extended_volumes = nova.api.openstack.compute.extended_volumes:ExtendedVolumes extension_info = nova.api.openstack.compute.extension_info:ExtensionInfo fixed_ips = nova.api.openstack.compute.fixed_ips:FixedIps flavors = nova.api.openstack.compute.flavors:Flavors @@ -106,7 +95,6 @@ nova.api.v21.extensions = floating_ips = nova.api.openstack.compute.floating_ips:FloatingIps floating_ips_bulk = nova.api.openstack.compute.floating_ips_bulk:FloatingIpsBulk fping = nova.api.openstack.compute.fping:Fping - hide_server_addresses = nova.api.openstack.compute.hide_server_addresses:HideServerAddresses hosts = nova.api.openstack.compute.hosts:Hosts hypervisors = nova.api.openstack.compute.hypervisors:Hypervisors images = nova.api.openstack.compute.images:Images @@ -117,19 +105,13 @@ nova.api.v21.extensions = ips = nova.api.openstack.compute.ips:IPs keypairs = nova.api.openstack.compute.keypairs:Keypairs limits = nova.api.openstack.compute.limits:Limits - lock_server = nova.api.openstack.compute.lock_server:LockServer - migrate_server = nova.api.openstack.compute.migrate_server:MigrateServer migrations = nova.api.openstack.compute.migrations:Migrations - multinic = nova.api.openstack.compute.multinic:Multinic multiple_create = nova.api.openstack.compute.multiple_create:MultipleCreate networks = nova.api.openstack.compute.networks:Networks networks_associate = nova.api.openstack.compute.networks_associate:NetworksAssociate - pause_server = nova.api.openstack.compute.pause_server:PauseServer quota_classes = nova.api.openstack.compute.quota_classes:QuotaClasses quota_sets = nova.api.openstack.compute.quota_sets:QuotaSets remote_consoles = nova.api.openstack.compute.remote_consoles:RemoteConsoles - rescue = nova.api.openstack.compute.rescue:Rescue - scheduler_hints = nova.api.openstack.compute.scheduler_hints:SchedulerHints security_group_default_rules = nova.api.openstack.compute.security_group_default_rules:SecurityGroupDefaultRules security_groups = nova.api.openstack.compute.security_groups:SecurityGroups server_diagnostics = nova.api.openstack.compute.server_diagnostics:ServerDiagnostics @@ -138,13 +120,9 @@ nova.api.v21.extensions = server_migrations = nova.api.openstack.compute.server_migrations:ServerMigrations server_password = nova.api.openstack.compute.server_password:ServerPassword server_tags = nova.api.openstack.compute.server_tags:ServerTags - server_usage = nova.api.openstack.compute.server_usage:ServerUsage server_groups = nova.api.openstack.compute.server_groups:ServerGroups - servers = nova.api.openstack.compute.servers:Servers services = nova.api.openstack.compute.services:Services - shelve = nova.api.openstack.compute.shelve:Shelve simple_tenant_usage = nova.api.openstack.compute.simple_tenant_usage:SimpleTenantUsage - suspend_server = nova.api.openstack.compute.suspend_server:SuspendServer tenant_networks = nova.api.openstack.compute.tenant_networks:TenantNetworks used_limits = nova.api.openstack.compute.used_limits:UsedLimits user_data = nova.api.openstack.compute.user_data:UserData