Merge "Make project_id optional in v2.1 urls"

This commit is contained in:
Jenkins
2016-01-25 02:42:36 +00:00
committed by Gerrit Code Review
17 changed files with 149 additions and 40 deletions

View File

@@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@@ -60,7 +60,13 @@ api_opts = [
'list. Specify the extension aliases here. ' 'list. Specify the extension aliases here. '
'This option will be removed in the near future. ' 'This option will be removed in the near future. '
'After that point you have to run all of the API.', 'After that point you have to run all of the API.',
deprecated_for_removal=True, deprecated_group='osapi_v21') deprecated_for_removal=True, deprecated_group='osapi_v21'),
cfg.StrOpt('project_id_regex',
default=None,
help='DEPRECATED: The validation regex for project_ids '
'used in urls. This defaults to [0-9a-f\-]+ if not set, '
'which matches normal uuids created by keystone.',
deprecated_for_removal=True, deprecated_group='osapi_v21')
] ]
api_opts_group = cfg.OptGroup(name='osapi_v21', title='API v2.1 Options') api_opts_group = cfg.OptGroup(name='osapi_v21', title='API v2.1 Options')
@@ -196,14 +202,40 @@ class APIMapper(routes.Mapper):
class ProjectMapper(APIMapper): class ProjectMapper(APIMapper):
def resource(self, member_name, collection_name, **kwargs): def resource(self, member_name, collection_name, **kwargs):
# 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.
project_id_regex = '[0-9a-f\-]+'
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
if 'parent_resource' not in kwargs: if 'parent_resource' not in kwargs:
kwargs['path_prefix'] = '{project_id}/' kwargs['path_prefix'] = '%s/' % project_id_token
else: else:
parent_resource = kwargs['parent_resource'] parent_resource = kwargs['parent_resource']
p_collection = parent_resource['collection_name'] p_collection = parent_resource['collection_name']
p_member = parent_resource['member_name'] p_member = parent_resource['member_name']
kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, kwargs['path_prefix'] = '%s/%s/:%s_id' % (
p_member) project_id_token,
p_collection,
p_member)
routes.Mapper.resource(
self,
member_name,
collection_name,
**kwargs)
# while we are in transition mode, create additional routes
# for the resource that do not include project_id.
if 'parent_resource' not in kwargs:
del kwargs['path_prefix']
else:
parent_resource = kwargs['parent_resource']
p_collection = parent_resource['collection_name']
p_member = parent_resource['member_name']
kwargs['path_prefix'] = '%s/:%s_id' % (p_collection,
p_member)
routes.Mapper.resource(self, member_name, routes.Mapper.resource(self, member_name,
collection_name, collection_name,
**kwargs) **kwargs)

View File

@@ -59,6 +59,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
* 2.15 - Add soft-affinity and soft-anti-affinity policies * 2.15 - Add soft-affinity and soft-anti-affinity policies
* 2.16 - Exposes host_status for servers/detail and servers/{server_id} * 2.16 - Exposes host_status for servers/detail and servers/{server_id}
* 2.17 - Add trigger_crash_dump to server actions * 2.17 - Add trigger_crash_dump to server actions
* 2.18 - Makes project_id optional in v2.1
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
@@ -67,7 +68,7 @@ REST_API_VERSION_HISTORY = """REST API Version History:
# Note(cyeoh): This only applies for the v2.1 API once microversions # Note(cyeoh): This only applies for the v2.1 API once microversions
# support is fully merged. It does not affect the V2 API. # support is fully merged. It does not affect the V2 API.
_MIN_API_VERSION = "2.1" _MIN_API_VERSION = "2.1"
_MAX_API_VERSION = "2.17" _MAX_API_VERSION = "2.18"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@@ -74,10 +74,14 @@ class NoAuthMiddleware(NoAuthMiddlewareBase):
return self.base_call(req, True, always_admin=False) return self.base_call(req, True, always_admin=False)
# TODO(johnthetubaguy) this should be removed in the M release class NoAuthMiddlewareV2_17(NoAuthMiddlewareBase):
class NoAuthMiddlewareV3(NoAuthMiddlewareBase): """Return a fake token if one isn't specified.
"""Return a fake token if one isn't specified."""
This provides a version of the middleware which does not add
project_id into server management urls.
"""
@webob.dec.wsgify(RequestClass=wsgi.Request) @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req): def __call__(self, req):
return self.base_call(req, False) return self.base_call(req, False, always_admin=False)

View File

@@ -154,3 +154,8 @@ class ImageMetadata(extensions.V21APIExtensionBase):
"/{project_id}/images/{image_id}/metadata", "/{project_id}/images/{image_id}/metadata",
controller=wsgi_resource, controller=wsgi_resource,
action='update_all', conditions={"method": ['PUT']}) action='update_all', conditions={"method": ['PUT']})
# Also connect the non project_id route
mapper.connect("metadata",
"/images/{image_id}/metadata",
controller=wsgi_resource,
action='update_all', conditions={"method": ['PUT']})

View File

@@ -191,3 +191,8 @@ class ServerMetadata(extensions.V21APIExtensionBase):
"/{project_id}/servers/{server_id}/metadata", "/{project_id}/servers/{server_id}/metadata",
controller=wsgi_resource, controller=wsgi_resource,
action='update_all', conditions={"method": ['PUT']}) action='update_all', conditions={"method": ['PUT']})
# Also connect the non project_id routes
mapper.connect("metadata",
"/servers/{server_id}/metadata",
controller=wsgi_resource,
action='update_all', conditions={"method": ['PUT']})

View File

@@ -163,3 +163,7 @@ user documentation.
Add a new API for triggering crash dump in an instance. Different operation Add a new API for triggering crash dump in an instance. Different operation
systems in instance may need different configurations to trigger crash dump. systems in instance may need different configurations to trigger crash dump.
2.18
----
Establishes a set of routes that makes project_id an optional construct in v2.1.

View File

@@ -54,3 +54,14 @@ class ApiPasteLegacyV2Fixture(ApiPasteV21Fixture):
"/v2: openstack_compute_api_v21_legacy_v2_compatible", "/v2: openstack_compute_api_v21_legacy_v2_compatible",
"/v2: openstack_compute_api_legacy_v2") "/v2: openstack_compute_api_legacy_v2")
target_file.write(line) target_file.write(line)
class ApiPasteNoProjectId(ApiPasteV21Fixture):
def _replace_line(self, target_file, line):
line = line.replace(
"paste.filter_factory = nova.api.openstack.auth:"
"NoAuthMiddleware.factory",
"paste.filter_factory = nova.api.openstack.auth:"
"NoAuthMiddlewareV2_17.factory")
target_file.write(line)

View File

@@ -69,6 +69,7 @@ class ApiSampleTestBaseV21(testscenarios.WithScenarios,
sample_dir = None sample_dir = None
extra_extensions_to_load = None extra_extensions_to_load = None
_legacy_v2_code = False _legacy_v2_code = False
_project_id = True
scenarios = [ scenarios = [
# test v2 with the v2.1 compatibility stack # test v2 with the v2.1 compatibility stack
@@ -82,7 +83,13 @@ class ApiSampleTestBaseV21(testscenarios.WithScenarios,
'api_major_version': 'v2', 'api_major_version': 'v2',
'_legacy_v2_code': True, '_legacy_v2_code': True,
'_additional_fixtures': [ '_additional_fixtures': [
api_paste_fixture.ApiPasteLegacyV2Fixture]}) api_paste_fixture.ApiPasteLegacyV2Fixture]}),
# test v2.16 code without project id
('v2_1_noproject_id', {
'api_major_version': 'v2.1',
'_project_id': False,
'_additional_fixtures': [
api_paste_fixture.ApiPasteNoProjectId]})
] ]
def setUp(self): def setUp(self):

View File

@@ -19,7 +19,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@@ -22,7 +22,7 @@
} }
], ],
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z" "updated": "2013-07-23T11:33:21Z"
} }

View File

@@ -250,9 +250,19 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
def _update_links(self, sample_data): def _update_links(self, sample_data):
"""Process sample data and update version specific links.""" """Process sample data and update version specific links."""
url_re = self._get_host() + "/v(2\.1|2)" # replace version urls
url_re = self._get_host() + "/v(2|2\.1)/openstack"
new_url = self._get_host() + "/" + self.api_major_version new_url = self._get_host() + "/" + self.api_major_version
if self._project_id:
new_url += "/openstack"
updated_data = re.sub(url_re, new_url, sample_data) updated_data = re.sub(url_re, new_url, sample_data)
# replace unversioned urls
url_re = self._get_host() + "/openstack"
new_url = self._get_host()
if self._project_id:
new_url += "/openstack"
updated_data = re.sub(url_re, new_url, updated_data)
return updated_data return updated_data
def _verify_response(self, name, subs, response, exp_code, def _verify_response(self, name, subs, response, exp_code,
@@ -360,13 +370,19 @@ class ApiSampleTestBase(integrated_helpers._IntegratedTestBase):
def _get_compute_endpoint(self): def _get_compute_endpoint(self):
# NOTE(sdague): "openstack" is stand in for project_id, it # NOTE(sdague): "openstack" is stand in for project_id, it
# should be more generic in future. # should be more generic in future.
return '%s/%s' % (self._get_host(), 'openstack') if self._project_id:
return '%s/%s' % (self._get_host(), 'openstack')
else:
return self._get_host()
def _get_vers_compute_endpoint(self): def _get_vers_compute_endpoint(self):
# NOTE(sdague): "openstack" is stand in for project_id, it # NOTE(sdague): "openstack" is stand in for project_id, it
# should be more generic in future. # should be more generic in future.
return '%s/%s/%s' % (self._get_host(), self.api_major_version, if self._project_id:
'openstack') return '%s/%s/%s' % (self._get_host(), self.api_major_version,
'openstack')
else:
return '%s/%s' % (self._get_host(), self.api_major_version)
def _get_response(self, url, method, body=None, strip_version=False, def _get_response(self, url, method, body=None, strip_version=False,
api_version=None, headers=None): api_version=None, headers=None):

View File

@@ -17,6 +17,8 @@
import webob import webob
import webob.dec import webob.dec
import testscenarios
from nova.api import openstack as openstack_api from nova.api import openstack as openstack_api
from nova.api.openstack import auth from nova.api.openstack import auth
from nova.api.openstack import compute from nova.api.openstack import compute
@@ -25,15 +27,29 @@ from nova import test
from nova.tests.unit.api.openstack import fakes from nova.tests.unit.api.openstack import fakes
class TestNoAuthMiddlewareV21(test.NoDBTestCase): class TestNoAuthMiddleware(testscenarios.WithScenarios, test.NoDBTestCase):
scenarios = [
('project_id', {
'expected_url': 'http://localhost/v2.1/user1_project',
'auth_middleware': auth.NoAuthMiddleware}),
('no_project_id', {
'expected_url': 'http://localhost/v2.1',
'auth_middleware': auth.NoAuthMiddlewareV2_17}),
]
def setUp(self): def setUp(self):
super(TestNoAuthMiddlewareV21, self).setUp() super(TestNoAuthMiddleware, self).setUp()
fakes.stub_out_rate_limiting(self.stubs) fakes.stub_out_rate_limiting(self.stubs)
fakes.stub_out_networking(self) fakes.stub_out_networking(self)
self.wsgi_app = fakes.wsgi_app_v21(use_no_auth=True) api_v21 = openstack_api.FaultWrapper(
self.req_url = '/v2' self.auth_middleware(
self.expected_url = "http://localhost/v2/user1_project" compute.APIRouterV21()
)
)
self.wsgi_app = urlmap.URLMap()
self.wsgi_app['/v2.1'] = api_v21
self.req_url = '/v2.1'
def test_authorize_user(self): def test_authorize_user(self):
req = webob.Request.blank(self.req_url) req = webob.Request.blank(self.req_url)
@@ -66,16 +82,3 @@ class TestNoAuthMiddlewareV21(test.NoDBTestCase):
self.assertEqual(result.status, '204 No Content') self.assertEqual(result.status, '204 No Content')
self.assertNotIn('X-CDN-Management-Url', result.headers) self.assertNotIn('X-CDN-Management-Url', result.headers)
self.assertNotIn('X-Storage-Url', result.headers) self.assertNotIn('X-Storage-Url', result.headers)
class TestNoAuthMiddlewareV3(TestNoAuthMiddlewareV21):
def setUp(self):
super(TestNoAuthMiddlewareV3, self).setUp()
api_router = compute.APIRouterV3()
api_v3 = openstack_api.FaultWrapper(auth.NoAuthMiddlewareV3(
api_router))
self.wsgi_app = urlmap.URLMap()
self.wsgi_app['/v3'] = api_v3
self.req_url = '/v3'
self.expected_url = "http://localhost/v3"

View File

@@ -66,7 +66,7 @@ EXP_VERSIONS = {
"v2.1": { "v2.1": {
"id": "v2.1", "id": "v2.1",
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z", "updated": "2013-07-23T11:33:21Z",
"links": [ "links": [
@@ -128,7 +128,7 @@ class VersionsTestV20(test.NoDBTestCase):
{ {
"id": "v2.1", "id": "v2.1",
"status": "CURRENT", "status": "CURRENT",
"version": "2.17", "version": "2.18",
"min_version": "2.1", "min_version": "2.1",
"updated": "2013-07-23T11:33:21Z", "updated": "2013-07-23T11:33:21Z",
"links": [ "links": [
@@ -194,7 +194,7 @@ class VersionsTestV20(test.NoDBTestCase):
self._test_get_version_2_detail('/', accept=accept) self._test_get_version_2_detail('/', accept=accept)
def test_get_version_2_versions_invalid(self): def test_get_version_2_versions_invalid(self):
req = webob.Request.blank('/v2/versions/1234') req = webob.Request.blank('/v2/versions/1234/foo')
req.accept = "application/json" req.accept = "application/json"
res = req.get_response(self.wsgi_app) res = req.get_response(self.wsgi_app)
self.assertEqual(404, res.status_int) self.assertEqual(404, res.status_int)
@@ -483,7 +483,7 @@ class VersionsTestV21(test.NoDBTestCase):
self.assertEqual(expected, version) self.assertEqual(expected, version)
def test_get_version_21_versions_invalid(self): def test_get_version_21_versions_invalid(self):
req = webob.Request.blank('/v2.1/versions/1234') req = webob.Request.blank('/v2.1/versions/1234/foo')
req.accept = "application/json" req.accept = "application/json"
res = req.get_response(fakes.wsgi_app_v21()) res = req.get_response(fakes.wsgi_app_v21())
self.assertEqual(404, res.status_int) self.assertEqual(404, res.status_int)

View File

@@ -63,6 +63,11 @@ class ConfFixture(config_fixture.Config):
group='api_database') group='api_database')
self.conf.set_default('fatal_exception_format_errors', True) self.conf.set_default('fatal_exception_format_errors', True)
self.conf.set_default('enabled', True, 'osapi_v21') self.conf.set_default('enabled', True, 'osapi_v21')
# TODO(sdague): this makes our project_id match 'fake' and
# 'openstack' as well. We should fix the tests to use real
# UUIDs then drop this work around.
self.conf.set_default('project_id_regex',
'[0-9a-fopnstk\-]+', 'osapi_v21')
self.conf.set_default('force_dhcp_release', False) self.conf.set_default('force_dhcp_release', False)
self.conf.set_default('periodic_enable', False) self.conf.set_default('periodic_enable', False)
policy_opts.set_defaults(self.conf) policy_opts.set_defaults(self.conf)

View File

@@ -0,0 +1,16 @@
---
features:
- Provides API 2.17, which makes the use of project_ids in API urls
optional.
upgrade:
- In order to make project_id optional in urls, we must constrain
the set of allowed values for project_id in our urls. This
defaults to a regex of ``[0-9a-f\-]+``, which will match hex uuids
(with / without dashes), and integers. This covers all known
project_id formats in the wild.
If your site uses other values for project_id, you can set a site
specific validation with ``project_id_regex`` config variable.