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
This commit is contained in:
parent
e93c2a3c25
commit
31b34e91e0
@ -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.
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -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))
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
|
@ -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 = [
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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\-]+")
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Inclusion of a project_id in API URLs is now optional. The `Block Storage
|
||||
API V3 <https://docs.openstack.org/api-ref/block-storage/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.
|
Loading…
Reference in New Issue
Block a user