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
This commit is contained in:
He Jie Xu 2017-03-28 23:19:47 +08:00
parent 658ee87995
commit 874ba55a49
10 changed files with 266 additions and 82 deletions

View File

@ -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):

View File

@ -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

View File

@ -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'])

View File

@ -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 []

View File

@ -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',

View File

@ -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

View File

@ -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,

View File

@ -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(

View File

@ -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():

View File

@ -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