From 31b34e91e0fe1fdc813a09b3c46a880b8d0e571f Mon Sep 17 00:00:00 2001 From: Alan Bishop Date: Tue, 8 Feb 2022 08:32:23 -0800 Subject: [PATCH] Remove the need for project_id from API endpoints Inclusion of a project_id in API URLs is now optional, and no longer required. Removing the project_id requirement facilitates supporting Secure RBAC's notion of system scope, in which an API method is not associated with a specific project. The API v3 routing is enhanced to provide duplicate routes for API methods that traditionally required a project_id in the URL: - The existing route for which a project_id is in the URL - A new route for when the URL does not include a project_id To test both routes and ensure there are no regresssions, the "API samples" functional tests include a project_id in the URLs, and the rest of the functional tests do not include the project_id. This is handled by changing the 'noauth' WSGI middleware to no longer add the project_id, and adding a new 'noauth_include_project_id' middleware filter that implements the legacy behavior. A new microversion V3.67 is introduced, but it only serves to inform clients whether the project_id is optional or required. When an API node supports mv 3.67, the project_id is optional in all API requests, even when the request specifies a earlier microversion. See the spec Ia44f199243be8f862520d7923007e7182b32f67d for more details on this behavior. Note: Much of the groundwork for this is based on manila's patch I5127e150e8a71e621890f30dba6720b3932cf583. DocImpact APIImpact Implements: blueprint project-id-optional-in-urls Change-Id: I3729cbe1902ab4dc335451d13ed921ec236fb8fd --- api-ref/source/v3/index.rst | 19 ++ .../versions/version-show-response.json | 4 +- .../samples/versions/versions-response.json | 4 +- cinder/api/common.py | 14 +- cinder/api/middleware/auth.py | 31 +++- cinder/api/openstack/__init__.py | 45 ++++- cinder/api/openstack/api_version_request.py | 5 +- .../openstack/rest_api_version_history.rst | 8 + cinder/api/v3/router.py | 58 +++--- cinder/common/config.py | 7 +- cinder/opts.py | 2 + .../tests/functional/api_samples_test_base.py | 16 ++ cinder/tests/unit/api/test_common.py | 41 ++++ cinder/tests/unit/api/v2/test_snapshots.py | 66 +++---- cinder/tests/unit/api/v2/test_volumes.py | 175 +++++++++--------- cinder/tests/unit/api/v3/test_messages.py | 6 +- cinder/tests/unit/api/v3/test_volumes.py | 6 +- cinder/tests/unit/conf_fixture.py | 2 + cinder/tests/unit/message/test_api.py | 6 +- .../block-storage/samples/api-paste.ini.inc | 4 + etc/cinder/api-paste.ini | 4 + ...-id-optional-in-urls-db97e2c447167853.yaml | 25 +++ 22 files changed, 377 insertions(+), 171 deletions(-) create mode 100644 releasenotes/notes/project-id-optional-in-urls-db97e2c447167853.yaml diff --git a/api-ref/source/v3/index.rst b/api-ref/source/v3/index.rst index e3474c15af3..ecd024ef635 100644 --- a/api-ref/source/v3/index.rst +++ b/api-ref/source/v3/index.rst @@ -4,6 +4,25 @@ Block Storage API V3 (CURRENT) ============================== +.. note:: + The URL for most API methods includes a {project_id} placeholder that + represents the caller's project ID. As of V3.67, the project_id is optional + in the URL, and the following are equivalent: + + * GET /v3/{project_id}/volumes + * GET /v3/volumes + + In both instances, the actual project_id used by the API method is the one + in the caller's keystone context. For that reason, including a project_id + in the URL is redundant. + + The V3.67 microversion is only used as an indicator that the API accepts a + URL without a project_id, and this applies to all requests regardless of + the microversion in the request. For example, an API node serving V3.67 or + greater will accept a URL without a project_id even if the request asks for + V3.0. Likewise, it will accept a URL containing a project_id even if the + request asks for V3.67. + .. rest_expand_all:: .. First thing we want to see is the version discovery document. diff --git a/api-ref/source/v3/samples/versions/version-show-response.json b/api-ref/source/v3/samples/versions/version-show-response.json index 63d41fad4a9..dc4d062341e 100644 --- a/api-ref/source/v3/samples/versions/version-show-response.json +++ b/api-ref/source/v3/samples/versions/version-show-response.json @@ -21,8 +21,8 @@ ], "min_version": "3.0", "status": "CURRENT", - "updated": "2021-09-16T00:00:00Z", - "version": "3.66" + "updated": "2021-12-16T00:00:00Z", + "version": "3.67" } ] } diff --git a/api-ref/source/v3/samples/versions/versions-response.json b/api-ref/source/v3/samples/versions/versions-response.json index 92bfafc5b98..59c049bfa00 100644 --- a/api-ref/source/v3/samples/versions/versions-response.json +++ b/api-ref/source/v3/samples/versions/versions-response.json @@ -21,8 +21,8 @@ ], "min_version": "3.0", "status": "CURRENT", - "updated": "2021-09-16T00:00:00Z", - "version": "3.66" + "updated": "2021-12-16T00:00:00Z", + "version": "3.67" } ] } diff --git a/cinder/api/common.py b/cinder/api/common.py index 9fc1de7b822..0439ecfc0b4 100644 --- a/cinder/api/common.py +++ b/cinder/api/common.py @@ -229,6 +229,14 @@ class ViewBuilder(object): _collection_name = None + def _get_project_id_in_url(self, request): + project_id = request.environ["cinder.context"].project_id + if project_id and ("/v3/%s" % project_id in request.url): + # project_ids are not mandatory within v3 URLs, but links need + # to include them if the request does. + return project_id + return '' + def _get_links(self, request, identifier): return [{"rel": "self", "href": self._get_href_link(request, identifier), }, @@ -242,7 +250,7 @@ class ViewBuilder(object): prefix = self._update_link_prefix(get_request_url(request), CONF.public_endpoint) url = os.path.join(prefix, - request.environ["cinder.context"].project_id, + self._get_project_id_in_url(request), collection_name) return "%s?%s" % (url, urllib.parse.urlencode(params)) @@ -251,7 +259,7 @@ class ViewBuilder(object): prefix = self._update_link_prefix(get_request_url(request), CONF.public_endpoint) return os.path.join(prefix, - request.environ["cinder.context"].project_id, + self._get_project_id_in_url(request), self._collection_name, str(identifier)) @@ -261,7 +269,7 @@ class ViewBuilder(object): base_url = self._update_link_prefix(base_url, CONF.public_endpoint) return os.path.join(base_url, - request.environ["cinder.context"].project_id, + self._get_project_id_in_url(request), self._collection_name, str(identifier)) diff --git a/cinder/api/middleware/auth.py b/cinder/api/middleware/auth.py index c0ccff008d4..caf36f65966 100644 --- a/cinder/api/middleware/auth.py +++ b/cinder/api/middleware/auth.py @@ -125,15 +125,17 @@ class CinderKeystoneContext(base_wsgi.Middleware): return self.application -class NoAuthMiddleware(base_wsgi.Middleware): +class NoAuthMiddlewareBase(base_wsgi.Middleware): """Return a fake token if one isn't specified.""" - @webob.dec.wsgify(RequestClass=wsgi.Request) - def __call__(self, req): + def base_call(self, req, project_id_in_path=False): if 'X-Auth-Token' not in req.headers: user_id = req.headers.get('X-Auth-User', 'admin') project_id = req.headers.get('X-Auth-Project-Id', 'admin') - os_url = os.path.join(req.url, project_id) + if project_id_in_path: + os_url = os.path.join(req.url.rstrip('/'), project_id) + else: + os_url = req.url.rstrip('/') res = webob.Response() # NOTE(vish): This is expecting and returning Auth(1.1), whereas # keystone uses 2.0 auth. We should probably allow @@ -157,3 +159,24 @@ class NoAuthMiddleware(base_wsgi.Middleware): req.environ['cinder.context'] = ctx return self.application + + +class NoAuthMiddleware(NoAuthMiddlewareBase): + """Return a fake token if one isn't specified. + + Sets project_id in URLs. + """ + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + return self.base_call(req) + + +class NoAuthMiddlewareIncludeProjectID(NoAuthMiddlewareBase): + """Return a fake token if one isn't specified. + + Does not set project_id in URLs. + """ + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + return self.base_call(req, project_id_in_path=True) diff --git a/cinder/api/openstack/__init__.py b/cinder/api/openstack/__init__.py index 3492f62fde9..fdc99c04096 100644 --- a/cinder/api/openstack/__init__.py +++ b/cinder/api/openstack/__init__.py @@ -18,6 +18,7 @@ WSGI middleware for OpenStack API controllers. """ +from oslo_config import cfg from oslo_log import log as logging from oslo_service import wsgi as base_wsgi import routes @@ -26,6 +27,16 @@ from cinder.api.openstack import wsgi from cinder.i18n import _ +openstack_api_opts = [ + cfg.StrOpt('project_id_regex', + default=r"[0-9a-f\-]+", + help=r'The validation regex for project_ids used in urls. ' + r'This defaults to [0-9a-f\\-]+ if not set, ' + r'which matches normal uuids created by keystone.'), +] + +CONF = cfg.CONF +CONF.register_opts(openstack_api_opts) LOG = logging.getLogger(__name__) @@ -48,14 +59,42 @@ class APIMapper(routes.Mapper): class ProjectMapper(APIMapper): def resource(self, member_name, collection_name, **kwargs): + """Base resource path handler + + This method is compatible with resource paths that include a + project_id and those that don't. Including project_id in the URLs + was a legacy API requirement; and making API requests against + such endpoints won't work for users that don't belong to a + particular project. + """ + # NOTE: project_id parameter is only valid if its hex or hex + dashes + # (note, integers are a subset of this). This is required to handle + # our overlapping routes issues. + project_id_regex = CONF.project_id_regex + project_id_token = '{project_id:%s}' % project_id_regex if 'parent_resource' not in kwargs: - kwargs['path_prefix'] = '{project_id}/' + kwargs['path_prefix'] = '%s/' % project_id_token else: parent_resource = kwargs['parent_resource'] p_collection = parent_resource['collection_name'] p_member = parent_resource['member_name'] - kwargs['path_prefix'] = '{project_id}/%s/:%s_id' % (p_collection, - p_member) + kwargs['path_prefix'] = '%s/%s/:%s_id' % (project_id_token, + p_collection, + p_member) + routes.Mapper.resource(self, + member_name, + collection_name, + **kwargs) + + # Add additional routes without 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, collection_name, diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index 183e904bf21..d7395b0e43e 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -152,14 +152,15 @@ REST_API_VERSION_HISTORY = """ - Accept 'consumes_quota' filter in volume and snapshot list operation. * 3.66 - Allow snapshotting in-use volumes without force flag. + * 3.67 - API URLs no longer need to include a project_id parameter. """ # The minimum and maximum versions of the API supported # The default api version request is defined to be the # minimum version of the API supported. _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.66" -UPDATED = "2021-09-16T00:00:00Z" +_MAX_API_VERSION = "3.67" +UPDATED = "2021-11-02T00:00:00Z" # NOTE(cyeoh): min and max versions declared as functions so we can diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index 666771a640c..bfba94e79b4 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -505,3 +505,11 @@ Volume snapshots of in-use volumes can be created without the 'force' flag. Although the 'force' flag is now considered invalid when passed in a volume snapshot request, for backward compatibility, the 'force' flag with a value evaluating to True is silently ignored. + +3.67 +---- +API URLs no longer need a "project_id" argument in them. For example, the API +route: ``https://$(controller)s/volume/v3/$(project_id)s/volumes`` is +equivalent to ``https://$(controller)s/volume/v3/volumes``. When interacting +with the cinder service as system or domain scoped users, a project_id should +not be specified in the API path. diff --git a/cinder/api/v3/router.py b/cinder/api/v3/router.py index 4ad2acfdb6c..e31c9b1f1a0 100644 --- a/cinder/api/v3/router.py +++ b/cinder/api/v3/router.py @@ -94,27 +94,31 @@ class APIRouter(cinder.api.openstack.APIRouter): controller=self.resources['groups'], collection={'detail': 'GET'}, member={'action': 'POST'}) - mapper.connect("groups", - "/{project_id}/groups/{id}/action", - controller=self.resources["groups"], - action="action", - conditions={"method": ["POST"]}) - mapper.connect("groups/action", - "/{project_id}/groups/action", - controller=self.resources["groups"], - action="action", - conditions={"method": ["POST"]}) + for path_prefix in ['/{project_id}', '']: + # project_id is optional + mapper.connect("groups", + "%s/groups/{id}/action" % path_prefix, + controller=self.resources["groups"], + action="action", + conditions={"method": ["POST"]}) + mapper.connect("groups/action", + "%s/groups/action" % path_prefix, + controller=self.resources["groups"], + action="action", + conditions={"method": ["POST"]}) self.resources['group_snapshots'] = group_snapshots.create_resource() mapper.resource("group_snapshot", "group_snapshots", controller=self.resources['group_snapshots'], collection={'detail': 'GET'}, member={'action': 'POST'}) - mapper.connect("group_snapshots", - "/{project_id}/group_snapshots/{id}/action", - controller=self.resources["group_snapshots"], - action="action", - conditions={"method": ["POST"]}) + for path_prefix in ['/{project_id}', '']: + # project_id is optional + mapper.connect("group_snapshots", + "%s/group_snapshots/{id}/action" % path_prefix, + controller=self.resources["group_snapshots"], + action="action", + conditions={"method": ["POST"]}) self.resources['snapshots'] = snapshots.create_resource(ext_mgr) mapper.resource("snapshot", "snapshots", controller=self.resources['snapshots'], @@ -134,11 +138,13 @@ class APIRouter(cinder.api.openstack.APIRouter): parent_resource=dict(member_name='snapshot', collection_name='snapshots')) - mapper.connect("metadata", - "/{project_id}/snapshots/{snapshot_id}/metadata", - controller=snapshot_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) + for path_prefix in ['/{project_id}', '']: + # project_id is optional + mapper.connect("metadata", + "%s/snapshots/{snapshot_id}/metadata" % path_prefix, + controller=snapshot_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) self.resources['volume_metadata'] = volume_metadata.create_resource() volume_metadata_controller = self.resources['volume_metadata'] @@ -148,11 +154,13 @@ class APIRouter(cinder.api.openstack.APIRouter): parent_resource=dict(member_name='volume', collection_name='volumes')) - mapper.connect("metadata", - "/{project_id}/volumes/{volume_id}/metadata", - controller=volume_metadata_controller, - action='update_all', - conditions={"method": ['PUT']}) + for path_prefix in ['/{project_id}', '']: + # project_id is optional + mapper.connect("metadata", + "%s/volumes/{volume_id}/metadata" % path_prefix, + controller=volume_metadata_controller, + action='update_all', + conditions={"method": ['PUT']}) self.resources['consistencygroups'] = ( consistencygroups.create_resource()) diff --git a/cinder/common/config.py b/cinder/common/config.py index fd7249f9f3c..12593281ae9 100644 --- a/cinder/common/config.py +++ b/cinder/common/config.py @@ -147,9 +147,12 @@ auth_opts = [ cfg.StrOpt('auth_strategy', default='keystone', choices=[('noauth', 'Do not perform authentication'), + ('noauth_include_project_id', + 'Do not perform authentication, and include a' + ' project_id in API URLs'), ('keystone', 'Authenticate using keystone')], - help='The strategy to use for auth. Supports noauth or ' - 'keystone.'), + help='The strategy to use for auth. Supports noauth,' + ' noauth_include_project_id or keystone.'), ] backup_opts = [ diff --git a/cinder/opts.py b/cinder/opts.py index e0343d5cd07..b9333a8063f 100644 --- a/cinder/opts.py +++ b/cinder/opts.py @@ -29,6 +29,7 @@ from cinder import objects # noqa objects.register_all() from cinder.api import common as cinder_api_common from cinder.api.middleware import auth as cinder_api_middleware_auth +import cinder.api.openstack from cinder.api.views import versions as cinder_api_views_versions from cinder.backup import api as cinder_backup_api from cinder.backup import chunkeddriver as cinder_backup_chunkeddriver @@ -216,6 +217,7 @@ def list_opts(): itertools.chain( cinder_api_common.api_common_opts, [cinder_api_middleware_auth.use_forwarded_for_opt], + cinder.api.openstack.openstack_api_opts, cinder_api_views_versions.versions_opts, cinder_backup_api.backup_opts, cinder_backup_chunkeddriver.backup_opts, diff --git a/cinder/tests/functional/api_samples_test_base.py b/cinder/tests/functional/api_samples_test_base.py index ca9ee9612c0..a5f6588415a 100644 --- a/cinder/tests/functional/api_samples_test_base.py +++ b/cinder/tests/functional/api_samples_test_base.py @@ -78,6 +78,22 @@ class ApiSampleTestBase(functional_helpers._FunctionalTestBase): # this is used to generate sample docs self.generate_samples = os.getenv('GENERATE_SAMPLES') is not None + def _get_flags(self): + f = super()._get_flags() + + # Use noauth_include_project_id so the API samples tests include a + # project_id in the API URLs. This is done for two reasons: + # + # 1. The API samples generated by the tests need to include a + # project_id because the API documentation includes the project_id. + # + # 2. It ensures there are no regressions, and the API functions + # correctly when a project_id is in the URL. The other functional + # tests do not include the project_id, so we cover both variants. + f['auth_strategy'] = {'v': 'noauth_include_project_id'} + + return f + @property def subs(self): return self._subs diff --git a/cinder/tests/unit/api/test_common.py b/cinder/tests/unit/api/test_common.py index e18bb24ccf6..21960911c70 100644 --- a/cinder/tests/unit/api/test_common.py +++ b/cinder/tests/unit/api/test_common.py @@ -25,6 +25,8 @@ import webob import webob.exc from cinder.api import common +from cinder.tests.unit.api import fakes +from cinder.tests.unit import fake_constants from cinder.tests.unit import test @@ -358,6 +360,45 @@ class TestCollectionLinks(test.TestCase): self._validate_next_link(item_count, osapi_max_limit, limit, should_link_exist) + @ddt.data( + { + # The project_id in the context matches the one in the v3 URL. + 'project_id': fake_constants.PROJECT_ID, + 'url': '/v3/%s/something' % fake_constants.PROJECT_ID, + 'expected': fake_constants.PROJECT_ID, + }, + { + # The project_id in the context does NOT match the one in the URL. + 'project_id': fake_constants.PROJECT2_ID, + 'url': '/v3/%s/something' % fake_constants.PROJECT_ID, + 'expected': '', + }, + { + # The context does not include a project_id (it's system scoped). + 'project_id': None, + 'url': '/v3/%s/something' % fake_constants.PROJECT_ID, + 'expected': '', + }, + { + # The v3 URL does not contain a project ID. + 'project_id': fake_constants.PROJECT_ID, + 'url': '/v3/something', + 'expected': '', + }, + { + # The URL doesn't specify v3. + 'project_id': fake_constants.PROJECT_ID, + 'url': '/vX/%s/something' % fake_constants.PROJECT_ID, + 'expected': '', + }, + ) + @ddt.unpack + def test_project_id_in_url(self, project_id, url, expected): + req = fakes.HTTPRequest.blank(url) + req.environ['cinder.context'].project_id = project_id + actual = common.ViewBuilder()._get_project_id_in_url(req) + self.assertEqual(expected, actual) + @ddt.ddt class GeneralFiltersTest(test.TestCase): diff --git a/cinder/tests/unit/api/v2/test_snapshots.py b/cinder/tests/unit/api/v2/test_snapshots.py index 18fc50ab836..ce58abb400f 100644 --- a/cinder/tests/unit/api/v2/test_snapshots.py +++ b/cinder/tests/unit/api/v2/test_snapshots.py @@ -102,7 +102,7 @@ class SnapshotApiTest(test.TestCase): } body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') resp_dict = self.controller.create(req, body=body) self.assertIn('snapshot', resp_dict) @@ -123,7 +123,7 @@ class SnapshotApiTest(test.TestCase): } body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') resp_dict = self.controller.create(req, body=body) self.assertIn('snapshot', resp_dict) @@ -144,7 +144,7 @@ class SnapshotApiTest(test.TestCase): "description": snapshot_description } body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') resp_dict = self.controller.create(req, body=body) self.assertIn('snapshot', resp_dict) @@ -169,7 +169,7 @@ class SnapshotApiTest(test.TestCase): "description": snapshot_description } body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') self.assertRaises(exception.InvalidVolume, self.controller.create, req, @@ -192,7 +192,7 @@ class SnapshotApiTest(test.TestCase): "description": snapshot_description } body = dict(snapshot=snapshot) - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') self.assertRaises(exception.ValidationError, self.controller.create, req, @@ -210,7 +210,7 @@ class SnapshotApiTest(test.TestCase): "description": snapshot_description } } - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) @@ -223,7 +223,7 @@ class SnapshotApiTest(test.TestCase): def test_snapshot_create_with_leading_trailing_spaces(self, body): volume = utils.create_volume(self.ctx, volume_type_id=None) body['snapshot']['volume_id'] = volume.id - req = fakes.HTTPRequest.blank('/v2/snapshots') + req = fakes.HTTPRequest.blank('/v3/snapshots') resp_dict = self.controller.create(req, body=body) self.assertEqual(body['snapshot']['display_name'].strip(), @@ -259,7 +259,7 @@ class SnapshotApiTest(test.TestCase): "name": "Updated Test Name", } body = {"snapshot": updates} - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) req.environ['cinder.context'] = self.ctx res_dict = self.controller.update(req, UUID, body=body) expected = { @@ -307,7 +307,7 @@ class SnapshotApiTest(test.TestCase): "description": None, } body = {"snapshot": updates} - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) req.environ['cinder.context'] = self.ctx res_dict = self.controller.update(req, UUID, body=body) @@ -318,13 +318,13 @@ class SnapshotApiTest(test.TestCase): def test_snapshot_update_missing_body(self): body = {} - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) self.assertRaises(exception.ValidationError, self.controller.update, req, UUID, body=body) def test_snapshot_update_invalid_body(self): body = {'name': 'missing top level snapshot key'} - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) self.assertRaises(exception.ValidationError, self.controller.update, req, UUID, body=body) @@ -334,7 +334,7 @@ class SnapshotApiTest(test.TestCase): "name": "Updated Test Name", } body = {"snapshot": updates} - req = fakes.HTTPRequest.blank('/v2/snapshots/not-the-uuid') + req = fakes.HTTPRequest.blank('/v3/snapshots/not-the-uuid') self.assertRaises(exception.SnapshotNotFound, self.controller.update, req, 'not-the-uuid', body=body) @@ -366,7 +366,7 @@ class SnapshotApiTest(test.TestCase): "description": " test " } body = {"snapshot": updates} - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) req.environ['cinder.context'] = self.ctx res_dict = self.controller.update(req, UUID, body=body) expected = { @@ -408,7 +408,7 @@ class SnapshotApiTest(test.TestCase): volume_get_by_id.return_value = fake_volume_obj snapshot_id = UUID - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % snapshot_id) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id) req.environ['cinder.context'] = self.ctx resp = self.controller.delete(req, snapshot_id) self.assertEqual(HTTPStatus.ACCEPTED, resp.status_int) @@ -417,7 +417,7 @@ class SnapshotApiTest(test.TestCase): self.mock_object(volume.api.API, "delete_snapshot", fake_snapshot_delete) snapshot_id = INVALID_UUID - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % snapshot_id) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id) self.assertRaises(exception.SnapshotNotFound, self.controller.delete, req, snapshot_id) @@ -439,7 +439,7 @@ class SnapshotApiTest(test.TestCase): fake_volume_obj = fake_volume.fake_volume_obj(self.ctx) snapshot_get_by_id.return_value = snapshot_obj volume_get_by_id.return_value = fake_volume_obj - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % UUID) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % UUID) req.environ['cinder.context'] = self.ctx resp_dict = self.controller.show(req, UUID) @@ -449,7 +449,7 @@ class SnapshotApiTest(test.TestCase): def test_snapshot_show_invalid_id(self): snapshot_id = INVALID_UUID - req = fakes.HTTPRequest.blank('/v2/snapshots/%s' % snapshot_id) + req = fakes.HTTPRequest.blank('/v3/snapshots/%s' % snapshot_id) self.assertRaises(exception.SnapshotNotFound, self.controller.show, req, snapshot_id) @@ -476,7 +476,7 @@ class SnapshotApiTest(test.TestCase): snapshots = objects.SnapshotList(objects=[snapshot_obj]) get_all_snapshots.return_value = snapshots - req = fakes.HTTPRequest.blank('/v2/snapshots/detail') + req = fakes.HTTPRequest.blank('/v3/snapshots/detail') resp_dict = self.controller.detail(req) self.assertIn('snapshots', resp_dict) @@ -494,7 +494,7 @@ class SnapshotApiTest(test.TestCase): @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) def test_admin_list_snapshots_limited_to_project(self, snapshot_metadata_get): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots' % fake.PROJECT_ID, + req = fakes.HTTPRequest.blank('/v3/%s/snapshots' % fake.PROJECT_ID, use_admin_context=True) res = self.controller.index(req) @@ -505,7 +505,7 @@ class SnapshotApiTest(test.TestCase): def test_list_snapshots_with_limit_and_offset(self, snapshot_metadata_get): def list_snapshots_with_limit_and_offset(snaps, is_admin): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots?limit=1' + req = fakes.HTTPRequest.blank('/v3/%s/snapshots?limit=1' '&offset=1' % fake.PROJECT_ID, use_admin_context=is_admin) res = self.controller.index(req) @@ -517,7 +517,7 @@ class SnapshotApiTest(test.TestCase): # Test that we get an empty list with an offset greater than the # number of items - req = fakes.HTTPRequest.blank('/v2/snapshots?limit=1&offset=3') + req = fakes.HTTPRequest.blank('/v3/snapshots?limit=1&offset=3') self.assertEqual({'snapshots': []}, self.controller.index(req)) volume, snaps = self._create_db_snapshots(3) @@ -535,32 +535,32 @@ class SnapshotApiTest(test.TestCase): mock_snapshot_get_all.return_value = [] # Negative limit - req = fakes.HTTPRequest.blank('/v2/snapshots?limit=-1&offset=1') + req = fakes.HTTPRequest.blank('/v3/snapshots?limit=-1&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) # Non numeric limit - req = fakes.HTTPRequest.blank('/v2/snapshots?limit=a&offset=1') + req = fakes.HTTPRequest.blank('/v3/snapshots?limit=a&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) # Negative offset - req = fakes.HTTPRequest.blank('/v2/snapshots?limit=1&offset=-1') + req = fakes.HTTPRequest.blank('/v3/snapshots?limit=1&offset=-1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) # Non numeric offset - req = fakes.HTTPRequest.blank('/v2/snapshots?limit=1&offset=a') + req = fakes.HTTPRequest.blank('/v3/snapshots?limit=1&offset=a') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) # Test that we get an exception HTTPBadRequest(400) with an offset # greater than the maximum offset value. - url = '/v2/snapshots?limit=1&offset=323245324356534235' + url = '/v3/snapshots?limit=1&offset=323245324356534235' req = fakes.HTTPRequest.blank(url) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) @@ -569,8 +569,8 @@ class SnapshotApiTest(test.TestCase): **kwargs): """Check a page of snapshots list.""" # Since we are accessing v2 api directly we don't need to specify - # v2 in the request path, if we did, we'd get /v2/v2 links back - request_path = '/v2/%s/snapshots' % project + # v2 in the request path, if we did, we'd get /v3/v2 links back + request_path = '/v3/%s/snapshots' % project expected_path = request_path # Construct the query if there are kwargs @@ -679,7 +679,7 @@ class SnapshotApiTest(test.TestCase): v2_fakes.fake_snapshot_get_all) @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) def test_admin_list_snapshots_all_tenants(self, snapshot_metadata_get): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots?all_tenants=1' % + req = fakes.HTTPRequest.blank('/v3/%s/snapshots?all_tenants=1' % fake.PROJECT_ID, use_admin_context=True) res = self.controller.index(req) @@ -700,7 +700,7 @@ class SnapshotApiTest(test.TestCase): snapshot_get_all.side_effect = get_all - req = fakes.HTTPRequest.blank('/v2/%s/snapshots?all_tenants=1' + req = fakes.HTTPRequest.blank('/v3/%s/snapshots?all_tenants=1' '&project_id=tenant1' % fake.PROJECT_ID, use_admin_context=True) res = self.controller.index(req) @@ -712,7 +712,7 @@ class SnapshotApiTest(test.TestCase): @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) def test_all_tenants_non_admin_gets_all_tenants(self, snapshot_metadata_get): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots?all_tenants=1' % + req = fakes.HTTPRequest.blank('/v3/%s/snapshots?all_tenants=1' % fake.PROJECT_ID) res = self.controller.index(req) self.assertIn('snapshots', res) @@ -724,13 +724,13 @@ class SnapshotApiTest(test.TestCase): v2_fakes.fake_snapshot_get_all) @mock.patch('cinder.db.snapshot_metadata_get', return_value=dict()) def test_non_admin_get_by_project(self, snapshot_metadata_get): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots' % fake.PROJECT_ID) + req = fakes.HTTPRequest.blank('/v3/%s/snapshots' % fake.PROJECT_ID) res = self.controller.index(req) self.assertIn('snapshots', res) self.assertEqual(1, len(res['snapshots'])) def _create_snapshot_bad_body(self, body): - req = fakes.HTTPRequest.blank('/v2/%s/snapshots' % fake.PROJECT_ID) + req = fakes.HTTPRequest.blank('/v3/%s/snapshots' % fake.PROJECT_ID) req.method = 'POST' self.assertRaises(exception.ValidationError, diff --git a/cinder/tests/unit/api/v2/test_volumes.py b/cinder/tests/unit/api/v2/test_volumes.py index a59d7cf6790..39e3e0768ed 100644 --- a/cinder/tests/unit/api/v2/test_volumes.py +++ b/cinder/tests/unit/api/v2/test_volumes.py @@ -93,7 +93,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body() body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller() self.assertEqual(ex, res_dict) @@ -112,7 +112,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(volume_type="FakeTypeName") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 404 when type name isn't valid self.assertRaises(exception.VolumeTypeNotFoundByName, self.controller.create, req, body=body) @@ -143,7 +143,7 @@ class VolumeApiTest(test.TestCase): db.sqlalchemy.api._GET_METHODS = {} self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/detail') + req = fakes.HTTPRequest.blank('/v3/volumes/detail') res_dict = self.controller.detail(req) self.assertTrue(mock_validate.called) @@ -207,11 +207,11 @@ class VolumeApiTest(test.TestCase): 'description': description, 'id': v2_fakes.DEFAULT_VOL_ID, 'links': - [{'href': 'http://localhost/v2/%s/volumes/%s' % ( - fake.PROJECT_ID, fake.VOLUME_ID), + [{'href': 'http://localhost/v3/volumes/%s' % ( + fake.VOLUME_ID), 'rel': 'self'}, - {'href': 'http://localhost/%s/volumes/%s' % ( - fake.PROJECT_ID, fake.VOLUME_ID), + {'href': 'http://localhost/volumes/%s' % ( + fake.VOLUME_ID), 'rel': 'bookmark'}], 'metadata': metadata, 'name': name, @@ -256,7 +256,7 @@ class VolumeApiTest(test.TestCase): snapshot_id = fake.SNAPSHOT_ID vol = self._vol_in_request_body(snapshot_id=snapshot_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller(snapshot_id=snapshot_id) @@ -282,7 +282,7 @@ class VolumeApiTest(test.TestCase): snapshot_id = fake.WILL_NOT_BE_FOUND_ID vol = self._vol_in_request_body(snapshot_id=snapshot_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 404 when snapshot cannot be found. self.assertRaises(exception.SnapshotNotFound, self.controller.create, req, body=body) @@ -297,7 +297,7 @@ class VolumeApiTest(test.TestCase): snapshot_id = value vol = self._vol_in_request_body(snapshot_id=snapshot_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 400 when snapshot has not uuid type. self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) @@ -315,7 +315,7 @@ class VolumeApiTest(test.TestCase): source_volid = '2f49aa3a-6aae-488d-8b99-a43271605af6' vol = self._vol_in_request_body(source_volid=source_volid) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller(source_volid=source_volid) @@ -344,7 +344,7 @@ class VolumeApiTest(test.TestCase): source_volid = fake.VOLUME_ID vol = self._vol_in_request_body(source_volid=source_volid) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 404 when source volume cannot be found. self.assertRaises(exception.VolumeNotFound, self.controller.create, req, body=body) @@ -361,7 +361,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body() vol.update(updated_uuids) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 400 for resource requested with invalid uuids. self.assertRaises(exception.ValidationError, self.controller.create, req, body=body) @@ -376,7 +376,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body( consistencygroup_id=consistencygroup_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') # Raise 404 when consistency group is not found. self.assertRaises(exception.GroupNotFound, self.controller.create, req, body=body) @@ -388,7 +388,7 @@ class VolumeApiTest(test.TestCase): def test_volume_creation_fails_with_bad_size(self): vol = self._vol_in_request_body(size="") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, req, @@ -397,7 +397,7 @@ class VolumeApiTest(test.TestCase): def test_volume_creation_fails_with_bad_availability_zone(self): vol = self._vol_in_request_body(availability_zone="zonen:hostn") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.InvalidAvailabilityZone, self.controller.create, req, body=body) @@ -415,7 +415,7 @@ class VolumeApiTest(test.TestCase): image_ref="c905cedb-7281-47e4-8a62-f26bc5fc4c77") ex = self._expected_vol_from_controller(availability_zone="nova") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) self.assertEqual(ex, res_dict) self.assertTrue(mock_validate.called) @@ -425,7 +425,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_ref=1234) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, req, @@ -439,7 +439,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_ref="12345") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, @@ -453,7 +453,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_ref="") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, @@ -472,7 +472,7 @@ class VolumeApiTest(test.TestCase): image_id="c905cedb-7281-47e4-8a62-f26bc5fc4c77") ex = self._expected_vol_from_controller(availability_zone="nova") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) self.assertEqual(ex, res_dict) self.assertTrue(mock_validate.called) @@ -482,7 +482,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_id=1234) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, req, @@ -496,7 +496,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_id="12345") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, @@ -510,7 +510,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="cinder", image_id="") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, @@ -532,7 +532,7 @@ class VolumeApiTest(test.TestCase): image_ref=test_id) ex = self._expected_vol_from_controller(availability_zone="nova") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) self.assertEqual(ex, res_dict) @@ -547,7 +547,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="nova", image_ref=test_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPConflict, self.controller.create, req, @@ -564,7 +564,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(availability_zone="nova", image_ref=test_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.create, req, @@ -573,7 +573,7 @@ class VolumeApiTest(test.TestCase): def test_volume_create_with_invalid_multiattach(self): vol = self._vol_in_request_body(multiattach="InvalidBool") body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, @@ -596,7 +596,7 @@ class VolumeApiTest(test.TestCase): ex = self._expected_vol_from_controller(multiattach=True) - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') res_dict = self.controller.create(req, body=body) self.assertEqual(ex, res_dict) @@ -609,7 +609,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body() vol['metadata'] = value body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/volumes') self.assertRaises(exception.ValidationError, self.controller.create, @@ -630,7 +630,7 @@ class VolumeApiTest(test.TestCase): "description": body['description'] } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertEqual(0, len(self.notifier.notifications)) name = updates["name"].strip() description = updates["description"].strip() @@ -655,7 +655,7 @@ class VolumeApiTest(test.TestCase): "display_description": "Updated Test Description", } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertEqual(0, len(self.notifier.notifications)) res_dict = self.controller.update(req, fake.VOLUME_ID, body=body) expected = self._expected_vol_from_controller( @@ -682,7 +682,7 @@ class VolumeApiTest(test.TestCase): "display_description": "Not Shown Description", } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertEqual(0, len(self.notifier.notifications)) res_dict = self.controller.update(req, fake.VOLUME_ID, body=body) expected = self._expected_vol_from_controller( @@ -705,7 +705,7 @@ class VolumeApiTest(test.TestCase): "metadata": {"qos_max_iops": '2000'} } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertEqual(0, len(self.notifier.notifications)) res_dict = self.controller.update(req, fake.VOLUME_ID, body=body) expected = self._expected_vol_from_controller( @@ -743,7 +743,7 @@ class VolumeApiTest(test.TestCase): "name": "Updated Test Name", } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertEqual(0, len(self.notifier.notifications)) admin_ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) req.environ['cinder.context'] = admin_ctx @@ -780,7 +780,7 @@ class VolumeApiTest(test.TestCase): "metadata": value } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.ValidationError, self.controller.update, @@ -788,7 +788,7 @@ class VolumeApiTest(test.TestCase): def test_update_empty_body(self): body = {} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.ValidationError, self.controller.update, req, fake.VOLUME_ID, body=body) @@ -797,7 +797,7 @@ class VolumeApiTest(test.TestCase): body = { 'name': 'missing top level volume key' } - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.ValidationError, self.controller.update, req, fake.VOLUME_ID, body=body) @@ -807,7 +807,7 @@ class VolumeApiTest(test.TestCase): {'display_name': 'a' * 256}, {'display_description': 'a' * 256}) def test_update_exceeds_length_name_description(self, vol): - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) body = {'volume': vol} self.assertRaises(exception.InvalidInput, self.controller.update, @@ -820,7 +820,7 @@ class VolumeApiTest(test.TestCase): "name": "Updated Test Name", } body = {"volume": updates} - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.VolumeNotFound, self.controller.update, req, fake.VOLUME_ID, body=body) @@ -831,7 +831,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes') + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) res_dict = self.controller.index(req) expected = { 'volumes': [ @@ -840,7 +840,7 @@ class VolumeApiTest(test.TestCase): 'id': fake.VOLUME_ID, 'links': [ { - 'href': 'http://localhost/v2/%s/volumes/%s' % ( + 'href': 'http://localhost/v3/%s/volumes/%s' % ( fake.PROJECT_ID, fake.VOLUME_ID), 'rel': 'self' }, @@ -863,7 +863,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/detail') + req = fakes.HTTPRequest.blank('/v3/volumes/detail') res_dict = self.controller.detail(req) exp_vol = self._expected_vol_from_controller( availability_zone=v2_fakes.DEFAULT_AZ, @@ -892,7 +892,7 @@ class VolumeApiTest(test.TestCase): attachment['id']) volume_tmp = db.volume_get(context.get_admin_context(), fake.VOLUME_ID) - req = fakes.HTTPRequest.blank('/v2/volumes/detail') + req = fakes.HTTPRequest.blank('/v3/volumes/detail') admin_ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) req.environ['cinder.context'] = admin_ctx res_dict = self.controller.detail(req) @@ -928,7 +928,7 @@ class VolumeApiTest(test.TestCase): db.volume_attachment_get(context.get_admin_context(), attachment['id']) - req = fakes.HTTPRequest.blank('/v2/volumes/detail') + req = fakes.HTTPRequest.blank('/v3/volumes/detail') res_dict = self.controller.detail(req) # host_name will always be None for non-admins self.assertIsNone( @@ -958,7 +958,7 @@ class VolumeApiTest(test.TestCase): fake_volume_get_all_by_project) self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get) - req = fakes.HTTPRequest.blank('/v2/volumes?marker=1') + req = fakes.HTTPRequest.blank('/v3/volumes?marker=1') res_dict = self.controller.index(req) volumes = res_dict['volumes'] self.assertEqual(2, len(volumes)) @@ -970,9 +970,9 @@ class VolumeApiTest(test.TestCase): v2_fakes.fake_volume_get_all_by_project) self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get) - req = fakes.HTTPRequest.blank('/v2/volumes' + req = fakes.HTTPRequest.blank('/v3/%s/volumes' '?limit=1&name=foo' - '&sort=id1:asc') + '&sort=id1:asc' % fake.PROJECT_ID) res_dict = self.controller.index(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) @@ -985,7 +985,7 @@ class VolumeApiTest(test.TestCase): links = res_dict['volumes_links'] self.assertEqual('next', links[0]['rel']) href_parts = urllib.parse.urlparse(links[0]['href']) - self.assertEqual('/v2/%s/volumes' % fake.PROJECT_ID, href_parts.path) + self.assertEqual('/v3/%s/volumes' % fake.PROJECT_ID, href_parts.path) params = urllib.parse.parse_qs(href_parts.query) self.assertEqual(str(volumes[0]['id']), params['marker'][0]) self.assertEqual('1', params['limit'][0]) @@ -993,13 +993,13 @@ class VolumeApiTest(test.TestCase): self.assertEqual('id1:asc', params['sort'][0]) def test_volume_index_limit_negative(self): - req = fakes.HTTPRequest.blank('/v2/volumes?limit=-1') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=-1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) def test_volume_index_limit_non_int(self): - req = fakes.HTTPRequest.blank('/v2/volumes?limit=a') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=a') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) @@ -1009,7 +1009,7 @@ class VolumeApiTest(test.TestCase): v2_fakes.fake_volume_get_all_by_project) self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get) - req = fakes.HTTPRequest.blank('/v2/volumes?marker=1&limit=1') + req = fakes.HTTPRequest.blank('/v3/volumes?marker=1&limit=1') res_dict = self.controller.index(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) @@ -1025,25 +1025,25 @@ class VolumeApiTest(test.TestCase): def test_volume_index_limit_offset(self): created_volumes = self._create_db_volumes(2) - req = fakes.HTTPRequest.blank('/v2/volumes?limit=2&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=2&offset=1') res_dict = self.controller.index(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) self.assertEqual(created_volumes[1].id, volumes[0]['id']) - req = fakes.HTTPRequest.blank('/v2/volumes?limit=-1&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=-1&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) - req = fakes.HTTPRequest.blank('/v2/volumes?limit=a&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=a&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, req) # Test that we get an exception HTTPBadRequest(400) with an offset # greater than the maximum offset value. - url = '/v2/volumes?limit=2&offset=43543564546567575' + url = '/v3/volumes?limit=2&offset=43543564546567575' req = fakes.HTTPRequest.blank(url) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.index, @@ -1066,7 +1066,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?marker=1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?marker=1') res_dict = self.controller.detail(req) volumes = res_dict['volumes'] self.assertEqual(2, len(volumes)) @@ -1078,7 +1078,8 @@ class VolumeApiTest(test.TestCase): v2_fakes.fake_volume_get_all_by_project) self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=1') + req = fakes.HTTPRequest.blank('/v3/%s/volumes/detail?limit=1' + % fake.PROJECT_ID) res_dict = self.controller.detail(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) @@ -1087,20 +1088,20 @@ class VolumeApiTest(test.TestCase): links = res_dict['volumes_links'] self.assertEqual('next', links[0]['rel']) href_parts = urllib.parse.urlparse(links[0]['href']) - self.assertEqual('/v2/%s/volumes/detail' % fake.PROJECT_ID, + self.assertEqual('/v3/%s/volumes/detail' % fake.PROJECT_ID, href_parts.path) params = urllib.parse.parse_qs(href_parts.query) self.assertIn('marker', params) self.assertEqual('1', params['limit'][0]) def test_volume_detail_limit_negative(self): - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=-1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=-1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, req) def test_volume_detail_limit_non_int(self): - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=a') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=a') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, req) @@ -1111,7 +1112,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?marker=1&limit=1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?marker=1&limit=1') res_dict = self.controller.detail(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) @@ -1119,30 +1120,30 @@ class VolumeApiTest(test.TestCase): def test_volume_detail_limit_offset(self): created_volumes = self._create_db_volumes(2) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=2&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=2&offset=1') res_dict = self.controller.detail(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) self.assertEqual(created_volumes[1].id, volumes[0]['id']) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=2&offset=1', + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=2&offset=1', use_admin_context=True) res_dict = self.controller.detail(req) volumes = res_dict['volumes'] self.assertEqual(1, len(volumes)) self.assertEqual(created_volumes[1].id, volumes[0]['id']) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=-1&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=-1&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, req) - req = fakes.HTTPRequest.blank('/v2/volumes/detail?limit=a&offset=1') + req = fakes.HTTPRequest.blank('/v3/volumes/detail?limit=a&offset=1') self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, req) - url = '/v2/volumes/detail?limit=2&offset=4536546546546467' + url = '/v3/volumes/detail?limit=2&offset=4536546546546467' req = fakes.HTTPRequest.blank(url) self.assertRaises(webob.exc.HTTPBadRequest, self.controller.detail, @@ -1152,7 +1153,7 @@ class VolumeApiTest(test.TestCase): def fake_volume_get_all(context, marker, limit, **kwargs): return [] self.mock_object(db, 'volume_get_all', fake_volume_get_all) - req = fakes.HTTPRequest.blank('/v2/volumes?limit=0') + req = fakes.HTTPRequest.blank('/v3/volumes?limit=0') res_dict = self.controller.index(req) expected = {'volumes': []} self.assertEqual(expected, res_dict) @@ -1163,7 +1164,7 @@ class VolumeApiTest(test.TestCase): ('volumes/detail', self.controller.detail)) key, fn = keys_fns[detailed] - req_string = '/v2/%s?all_tenants=1' % key + req_string = '/v3/%s?all_tenants=1' % key if limit: req_string += '&limit=%s' % limit req = fakes.HTTPRequest.blank(req_string, use_admin_context=True) @@ -1255,7 +1256,7 @@ class VolumeApiTest(test.TestCase): # all_tenants does not matter for non-admin for params in ['', '?all_tenants=1']: - req = fakes.HTTPRequest.blank('/v2/volumes%s' % params) + req = fakes.HTTPRequest.blank('/v3/volumes%s' % params) resp = self.controller.index(req) self.assertEqual(1, len(resp['volumes'])) self.assertEqual('vol1', resp['volumes'][0]['name']) @@ -1280,7 +1281,7 @@ class VolumeApiTest(test.TestCase): fake_volume_get_all_by_project2) self.mock_object(db, 'volume_get_all', fake_volume_get_all2) - req = fakes.HTTPRequest.blank('/v2/volumes', use_admin_context=True) + req = fakes.HTTPRequest.blank('/v3/volumes', use_admin_context=True) resp = self.controller.index(req) self.assertEqual(1, len(resp['volumes'])) self.assertEqual('vol2', resp['volumes'][0]['name']) @@ -1306,7 +1307,7 @@ class VolumeApiTest(test.TestCase): fake_volume_get_all_by_project3) self.mock_object(db, 'volume_get_all', fake_volume_get_all3) - req = fakes.HTTPRequest.blank('/v2/volumes?all_tenants=1', + req = fakes.HTTPRequest.blank('/v3/volumes?all_tenants=1', use_admin_context=True) resp = self.controller.index(req) self.assertEqual(1, len(resp['volumes'])) @@ -1317,7 +1318,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) res_dict = self.controller.show(req, fake.VOLUME_ID) expected = self._expected_vol_from_controller( availability_zone=v2_fakes.DEFAULT_AZ, @@ -1344,7 +1345,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) res_dict = self.controller.show(req, fake.VOLUME_ID) expected = self._expected_vol_from_controller( availability_zone=v2_fakes.DEFAULT_AZ, @@ -1356,7 +1357,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(volume_api.API, "get", v2_fakes.fake_volume_get_notfound) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.VolumeNotFound, self.controller.show, req, 1) # Finally test that nothing was cached @@ -1380,7 +1381,7 @@ class VolumeApiTest(test.TestCase): attach_tmp = db.volume_attachment_get(context.get_admin_context(), attachment['id']) volume_tmp = db.volume_get(context.get_admin_context(), fake.VOLUME_ID) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) admin_ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID, True) req.environ['cinder.context'] = admin_ctx res_dict = self.controller.show(req, fake.VOLUME_ID) @@ -1412,7 +1413,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) res_dict = self.controller.show(req, fake.VOLUME_ID) self.assertTrue(res_dict['volume']['encrypted']) @@ -1421,7 +1422,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) res_dict = self.controller.show(req, fake.VOLUME_ID) self.assertEqual(False, res_dict['volume']['encrypted']) @@ -1435,14 +1436,14 @@ class VolumeApiTest(test.TestCase): self.mock_object(db.sqlalchemy.api, '_volume_type_get_full', v2_fakes.fake_volume_type_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) res_dict = self.controller.show(req, fake.VOLUME_ID) self.assertEqual('deleting', res_dict['volume']['status']) @mock.patch.object(volume_api.API, 'delete', v2_fakes.fake_volume_delete) @mock.patch.object(volume_api.API, 'get', v2_fakes.fake_volume_get) def test_volume_delete(self): - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) resp = self.controller.delete(req, fake.VOLUME_ID) self.assertEqual(HTTPStatus.ACCEPTED, resp.status_int) @@ -1453,7 +1454,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(volume_api.API, "delete", fake_volume_attached) self.mock_object(volume_api.API, 'get', v2_fakes.fake_volume_get) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) exp = self.assertRaises(exception.VolumeAttached, self.controller.delete, req, 1) @@ -1464,7 +1465,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(volume_api.API, "get", v2_fakes.fake_volume_get_notfound) - req = fakes.HTTPRequest.blank('/v2/volumes/%s' % fake.VOLUME_ID) + req = fakes.HTTPRequest.blank('/v3/volumes/%s' % fake.VOLUME_ID) self.assertRaises(exception.VolumeNotFound, self.controller.delete, req, 1) @@ -1472,7 +1473,7 @@ class VolumeApiTest(test.TestCase): self.mock_object(db, 'volume_get_all_by_project', v2_fakes.fake_volume_get_all_by_project) - req = fakes.HTTPRequest.blank('/v2/%s/volumes' % fake.PROJECT_ID, + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID, use_admin_context=True) res = self.controller.index(req) @@ -1484,7 +1485,7 @@ class VolumeApiTest(test.TestCase): v2_fakes.fake_volume_get_all_by_project) def test_admin_list_volumes_all_tenants(self): req = fakes.HTTPRequest.blank( - '/v2/%s/volumes?all_tenants=1' % fake.PROJECT_ID, + '/v3/%s/volumes?all_tenants=1' % fake.PROJECT_ID, use_admin_context=True) res = self.controller.index(req) self.assertIn('volumes', res) @@ -1496,7 +1497,7 @@ class VolumeApiTest(test.TestCase): @mock.patch.object(volume_api.API, 'get', v2_fakes.fake_volume_get) def test_all_tenants_non_admin_gets_all_tenants(self): req = fakes.HTTPRequest.blank( - '/v2/%s/volumes?all_tenants=1' % fake.PROJECT_ID) + '/v3/%s/volumes?all_tenants=1' % fake.PROJECT_ID) res = self.controller.index(req) self.assertIn('volumes', res) self.assertEqual(1, len(res['volumes'])) @@ -1505,13 +1506,13 @@ class VolumeApiTest(test.TestCase): v2_fakes.fake_volume_get_all_by_project) @mock.patch.object(volume_api.API, 'get', v2_fakes.fake_volume_get) def test_non_admin_get_by_project(self): - req = fakes.HTTPRequest.blank('/v2/%s/volumes' % fake.PROJECT_ID) + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) res = self.controller.index(req) self.assertIn('volumes', res) self.assertEqual(1, len(res['volumes'])) def _create_volume_bad_request(self, body): - req = fakes.HTTPRequest.blank('/v2/%s/volumes' % fake.PROJECT_ID) + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) req.method = 'POST' self.assertRaises(exception.ValidationError, diff --git a/cinder/tests/unit/api/v3/test_messages.py b/cinder/tests/unit/api/v3/test_messages.py index a4de9cd1037..6a260ee249c 100644 --- a/cinder/tests/unit/api/v3/test_messages.py +++ b/cinder/tests/unit/api/v3/test_messages.py @@ -68,7 +68,8 @@ class MessageApiTest(test.TestCase): self.mock_object(message_api.API, 'get', v3_fakes.fake_message_get) req = fakes.HTTPRequest.blank( - '/v3/messages/%s' % fakes.FAKE_UUID, version=mv.MESSAGES) + '/v3/fakeproject/messages/%s' % fakes.FAKE_UUID, + version=mv.MESSAGES) req.environ['cinder.context'] = self.ctxt res_dict = self.controller.show(req, fakes.FAKE_UUID) @@ -142,7 +143,8 @@ class MessageApiTest(test.TestCase): self.mock_object(message_api.API, 'get_all', return_value=[v3_fakes.fake_message(fakes.FAKE_UUID)]) req = fakes.HTTPRequest.blank( - '/v3/messages/%s' % fakes.FAKE_UUID, version=mv.MESSAGES) + '/v3/fakeproject/messages/%s' % fakes.FAKE_UUID, + version=mv.MESSAGES) req.environ['cinder.context'] = self.ctxt res_dict = self.controller.index(req) diff --git a/cinder/tests/unit/api/v3/test_volumes.py b/cinder/tests/unit/api/v3/test_volumes.py index ac1c11ccd62..f20e61d65fb 100644 --- a/cinder/tests/unit/api/v3/test_volumes.py +++ b/cinder/tests/unit/api/v3/test_volumes.py @@ -412,7 +412,7 @@ class VolumeApiTest(test.TestCase): snapshot_id = fake.SNAPSHOT_ID ex = self._expected_vol_from_controller(snapshot_id=snapshot_id) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v3/volumes') + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) req.headers = mv.get_mv_header(mv.SUPPORT_NOVA_IMAGE) req.api_version_request = mv.get_api_version(mv.SUPPORT_NOVA_IMAGE) res_dict = self.controller.create(req, body=body) @@ -736,7 +736,7 @@ class VolumeApiTest(test.TestCase): vol = self._vol_in_request_body(snapshot_id=snapshot_id, group_id=fake.GROUP_ID) body = {"volume": vol} - req = fakes.HTTPRequest.blank('/v3/volumes') + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) req.api_version_request = mv.get_api_version(max_ver) res_dict = self.controller.create(req, body=body) ex = self._expected_vol_from_controller( @@ -771,7 +771,7 @@ class VolumeApiTest(test.TestCase): volume_type_get.side_effect = v2_fakes.fake_volume_type_get backup_id = fake.BACKUP_ID - req = fakes.HTTPRequest.blank('/v3/volumes') + req = fakes.HTTPRequest.blank('/v3/%s/volumes' % fake.PROJECT_ID) req.api_version_request = mv.get_api_version(max_ver) if max_ver == mv.VOLUME_CREATE_FROM_BACKUP: vol = self._vol_in_request_body(backup_id=backup_id) diff --git a/cinder/tests/unit/conf_fixture.py b/cinder/tests/unit/conf_fixture.py index 918c80f79a4..0fc70cbd1b0 100644 --- a/cinder/tests/unit/conf_fixture.py +++ b/cinder/tests/unit/conf_fixture.py @@ -57,3 +57,5 @@ def set_defaults(conf): # This is where we don't authenticate conf.set_default('auth_strategy', 'noauth') conf.set_default('auth_url', 'fake', 'keystone_authtoken') + # we use "fake" and "openstack" as project ID in a number of tests + conf.set_default('project_id_regex', r"[0-9a-fopnstk\-]+") diff --git a/cinder/tests/unit/message/test_api.py b/cinder/tests/unit/message/test_api.py index ed43713fb24..d9ffc907072 100644 --- a/cinder/tests/unit/message/test_api.py +++ b/cinder/tests/unit/message/test_api.py @@ -469,7 +469,7 @@ class MessageApiTest(test.TestCase): self.create_message_for_tests() # first request of this test - url = '/v3/fake/messages?limit=2' + url = '/v3/%s/messages?limit=2' % fake_constants.PROJECT_ID req = fakes.HTTPRequest.blank(url) req.method = 'GET' req.content_type = 'application/json' @@ -489,8 +489,8 @@ class MessageApiTest(test.TestCase): # Second request in this test # Test for second page using marker (res['messages][0]['id']) # values fetched in first request with limit 2 in this test - url = '/v3/fake/messages?limit=1&marker=%s' % ( - res['messages'][0]['id']) + url = '/v3/%s/messages?limit=1&marker=%s' % ( + fake_constants.PROJECT_ID, res['messages'][0]['id']) req = fakes.HTTPRequest.blank(url) req.method = 'GET' req.content_type = 'application/json' diff --git a/doc/source/configuration/block-storage/samples/api-paste.ini.inc b/doc/source/configuration/block-storage/samples/api-paste.ini.inc index 58bc48fd38e..cf3ba071e17 100644 --- a/doc/source/configuration/block-storage/samples/api-paste.ini.inc +++ b/doc/source/configuration/block-storage/samples/api-paste.ini.inc @@ -10,6 +10,7 @@ use = call:cinder.api:root_app_factory [composite:openstack_volume_api_v3] use = call:cinder.api.middleware.auth:pipeline_factory noauth = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler noauth apiv3 +noauth_include_project_id = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler noauth_include_project_id apiv3 keystone = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3 keystone_nolimit = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3 @@ -32,6 +33,9 @@ paste.filter_factory = osprofiler.web:WsgiMiddleware.factory [filter:noauth] paste.filter_factory = cinder.api.middleware.auth:NoAuthMiddleware.factory +[filter:noauth_include_project_id] +paste.filter_factory = cinder.api.middleware.auth:NoAuthMiddlewareIncludeProjectID.factory + [filter:sizelimit] paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory diff --git a/etc/cinder/api-paste.ini b/etc/cinder/api-paste.ini index 3fbe21b4ec9..ce2e0ea98b6 100644 --- a/etc/cinder/api-paste.ini +++ b/etc/cinder/api-paste.ini @@ -11,6 +11,7 @@ use = call:cinder.api:root_app_factory [composite:openstack_volume_api_v3] use = call:cinder.api.middleware.auth:pipeline_factory noauth = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler noauth apiv3 +noauth_include_project_id = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler noauth_include_project_id apiv3 keystone = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3 keystone_nolimit = cors http_proxy_to_wsgi request_id faultwrap sizelimit osprofiler authtoken keystonecontext apiv3 @@ -33,6 +34,9 @@ paste.filter_factory = osprofiler.web:WsgiMiddleware.factory [filter:noauth] paste.filter_factory = cinder.api.middleware.auth:NoAuthMiddleware.factory +[filter:noauth_include_project_id] +paste.filter_factory = cinder.api.middleware.auth:NoAuthMiddlewareIncludeProjectID.factory + [filter:sizelimit] paste.filter_factory = oslo_middleware.sizelimit:RequestBodySizeLimiter.factory diff --git a/releasenotes/notes/project-id-optional-in-urls-db97e2c447167853.yaml b/releasenotes/notes/project-id-optional-in-urls-db97e2c447167853.yaml new file mode 100644 index 00000000000..018a8b644c9 --- /dev/null +++ b/releasenotes/notes/project-id-optional-in-urls-db97e2c447167853.yaml @@ -0,0 +1,25 @@ +--- +features: + - | + Inclusion of a project_id in API URLs is now optional. The `Block Storage + API V3 `_ reference + guide continues to show URLs with a project_id because the legacy behavior + continues to be supported. + + A new API microversion V3.67 is introduced to inform clients when + inclusion of a project_id in API URLs is optional. The V3.67 microversion + is only used as an indicator that the API accepts a URL without a + project_id, and this applies to all requests regardless of the + microversion in the request. For example, an API node serving V3.67 or + greater will accept a URL without a project_id even if the request asks + for V3.0. Likewise, it will accept a URL containing a project_id even if + the request asks for V3.67. +upgrade: + - | + Upgrades are not affected by the new functionality whereby a project_id + is no longer required in API URLs. The legacy behavior in which a + project_id is included in the URL continues to be supported. + + Detection of whether a URL includes a project_id is based on the value of + a new ``project_id_regex`` option. The default value matches UUIDs + created by keystone.