V3 jsonschema validation: volume actions
This patch adds jsonschema validation for below volume actions API's * POST /v3/{project_id}/volumes/{volume_id}/action (attach) * POST /v3/{project_id}/volumes/{volume_id}/action (detach) * POST /v3/{project_id}/volumes/{volume_id}/action (volume_upload_image) * POST /v3/{project_id}/volumes/{volume_id}/action (extend) * POST /v3/{project_id}/volumes/{volume_id}/action (retype) * POST /v3/{project_id}/volumes/{volume_id}/action (initialize_connection) * POST /v3/{project_id}/volumes/{volume_id}/action (terminate_connection) * POST /v3/{project_id}/volumes/{volume_id}/action (set_bootable_status) * POST /v3/{project_id}/volumes/{volume_id}/action (update_readonly_flag) Change-Id: I39ede009d5e909a076860df7305865286caa5352 Partial-Implements: bp json-schema-validation
This commit is contained in:
parent
3e8161a582
commit
d209139227
@ -16,7 +16,6 @@
|
|||||||
from castellan import key_manager
|
from castellan import key_manager
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import oslo_messaging as messaging
|
import oslo_messaging as messaging
|
||||||
from oslo_utils import encodeutils
|
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
import six
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
@ -25,11 +24,11 @@ import webob
|
|||||||
from cinder.api import extensions
|
from cinder.api import extensions
|
||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
|
from cinder.api.schemas import volume_actions as volume_action
|
||||||
|
from cinder.api import validation
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder.image import image_utils
|
|
||||||
from cinder.policies import volume_actions as policy
|
from cinder.policies import volume_actions as policy
|
||||||
from cinder import utils
|
|
||||||
from cinder import volume
|
from cinder import volume
|
||||||
|
|
||||||
|
|
||||||
@ -52,6 +51,7 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-attach')
|
@wsgi.action('os-attach')
|
||||||
|
@validation.schema(volume_action.attach)
|
||||||
def _attach(self, req, id, body):
|
def _attach(self, req, id, body):
|
||||||
"""Add attachment metadata."""
|
"""Add attachment metadata."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
@ -66,23 +66,9 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
# Keep API backward compatibility
|
# Keep API backward compatibility
|
||||||
if 'host_name' in body['os-attach']:
|
if 'host_name' in body['os-attach']:
|
||||||
host_name = body['os-attach']['host_name']
|
host_name = body['os-attach']['host_name']
|
||||||
if 'mountpoint' not in body['os-attach']:
|
|
||||||
msg = _("Must specify 'mountpoint'")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
mountpoint = body['os-attach']['mountpoint']
|
mountpoint = body['os-attach']['mountpoint']
|
||||||
if 'mode' in body['os-attach']:
|
mode = body['os-attach'].get('mode', 'rw')
|
||||||
mode = body['os-attach']['mode']
|
|
||||||
else:
|
|
||||||
mode = 'rw'
|
|
||||||
|
|
||||||
if instance_uuid is None and host_name is None:
|
|
||||||
msg = _("Invalid request to attach volume to an invalid target")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
if mode not in ('rw', 'ro'):
|
|
||||||
msg = _("Invalid request to attach volume with an invalid mode. "
|
|
||||||
"Attaching mode should be 'rw' or 'ro'")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
try:
|
try:
|
||||||
self.volume_api.attach(context, volume,
|
self.volume_api.attach(context, volume,
|
||||||
instance_uuid, host_name, mountpoint, mode)
|
instance_uuid, host_name, mountpoint, mode)
|
||||||
@ -101,6 +87,7 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-detach')
|
@wsgi.action('os-detach')
|
||||||
|
@validation.schema(volume_action.detach)
|
||||||
def _detach(self, req, id, body):
|
def _detach(self, req, id, body):
|
||||||
"""Clear attachment metadata."""
|
"""Clear attachment metadata."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
@ -108,7 +95,6 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
|
|
||||||
attachment_id = None
|
attachment_id = None
|
||||||
if body['os-detach']:
|
|
||||||
attachment_id = body['os-detach'].get('attachment_id', None)
|
attachment_id = body['os-detach'].get('attachment_id', None)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -166,16 +152,13 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
self.volume_api.roll_detaching(context, volume)
|
self.volume_api.roll_detaching(context, volume)
|
||||||
|
|
||||||
@wsgi.action('os-initialize_connection')
|
@wsgi.action('os-initialize_connection')
|
||||||
|
@validation.schema(volume_action.initialize_connection)
|
||||||
def _initialize_connection(self, req, id, body):
|
def _initialize_connection(self, req, id, body):
|
||||||
"""Initialize volume attachment."""
|
"""Initialize volume attachment."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
try:
|
|
||||||
connector = body['os-initialize_connection']['connector']
|
connector = body['os-initialize_connection']['connector']
|
||||||
except KeyError:
|
|
||||||
raise webob.exc.HTTPBadRequest(
|
|
||||||
explanation=_("Must specify 'connector'"))
|
|
||||||
try:
|
try:
|
||||||
info = self.volume_api.initialize_connection(context,
|
info = self.volume_api.initialize_connection(context,
|
||||||
volume,
|
volume,
|
||||||
@ -195,16 +178,13 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-terminate_connection')
|
@wsgi.action('os-terminate_connection')
|
||||||
|
@validation.schema(volume_action.terminate_connection)
|
||||||
def _terminate_connection(self, req, id, body):
|
def _terminate_connection(self, req, id, body):
|
||||||
"""Terminate volume attachment."""
|
"""Terminate volume attachment."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
try:
|
|
||||||
connector = body['os-terminate_connection']['connector']
|
connector = body['os-terminate_connection']['connector']
|
||||||
except KeyError:
|
|
||||||
raise webob.exc.HTTPBadRequest(
|
|
||||||
explanation=_("Must specify 'connector'"))
|
|
||||||
try:
|
try:
|
||||||
self.volume_api.terminate_connection(context, volume, connector)
|
self.volume_api.terminate_connection(context, volume, connector)
|
||||||
except exception.VolumeBackendAPIException:
|
except exception.VolumeBackendAPIException:
|
||||||
@ -213,37 +193,23 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-volume_upload_image')
|
@wsgi.action('os-volume_upload_image')
|
||||||
|
@validation.schema(volume_action.volume_upload_image, '2.0', '3.0')
|
||||||
|
@validation.schema(volume_action.volume_upload_image_v31, '3.1')
|
||||||
def _volume_upload_image(self, req, id, body):
|
def _volume_upload_image(self, req, id, body):
|
||||||
"""Uploads the specified volume to image service."""
|
"""Uploads the specified volume to image service."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
params = body['os-volume_upload_image']
|
params = body['os-volume_upload_image']
|
||||||
req_version = req.api_version_request
|
req_version = req.api_version_request
|
||||||
if not params.get("image_name"):
|
|
||||||
msg = _("No image_name was specified in request.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
force = params.get('force', 'False')
|
force = params.get('force', 'False')
|
||||||
try:
|
|
||||||
force = strutils.bool_from_string(force, strict=True)
|
force = strutils.bool_from_string(force, strict=True)
|
||||||
except ValueError as error:
|
|
||||||
err_msg = encodeutils.exception_to_unicode(error)
|
|
||||||
msg = _("Invalid value for 'force': '%s'") % err_msg
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
|
|
||||||
context.authorize(policy.UPLOAD_IMAGE_POLICY, target_obj=volume)
|
context.authorize(policy.UPLOAD_IMAGE_POLICY)
|
||||||
# check for valid disk-format
|
# check for valid disk-format
|
||||||
disk_format = params.get("disk_format", "raw")
|
disk_format = params.get("disk_format", "raw")
|
||||||
if not image_utils.validate_disk_format(disk_format):
|
|
||||||
msg = _("Invalid disk-format '%(disk_format)s' is specified. "
|
|
||||||
"Allowed disk-formats are %(allowed_disk_formats)s.") % {
|
|
||||||
"disk_format": disk_format,
|
|
||||||
"allowed_disk_formats": ", ".join(
|
|
||||||
image_utils.VALID_DISK_FORMATS)
|
|
||||||
}
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
image_metadata = {"container_format": params.get(
|
image_metadata = {"container_format": params.get(
|
||||||
"container_format", "bare"),
|
"container_format", "bare"),
|
||||||
@ -267,14 +233,11 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
mv.UPLOAD_IMAGE_PARAMS):
|
mv.UPLOAD_IMAGE_PARAMS):
|
||||||
|
|
||||||
image_metadata['visibility'] = params.get('visibility', 'private')
|
image_metadata['visibility'] = params.get('visibility', 'private')
|
||||||
image_metadata['protected'] = params.get('protected', 'False')
|
image_metadata['protected'] = strutils.bool_from_string(
|
||||||
|
params.get('protected', 'False'), strict=True)
|
||||||
|
|
||||||
if image_metadata['visibility'] == 'public':
|
if image_metadata['visibility'] == 'public':
|
||||||
context.authorize(policy.UPLOAD_PUBLIC_POLICY,
|
context.authorize(policy.UPLOAD_PUBLIC_POLICY)
|
||||||
target_obj=volume)
|
|
||||||
|
|
||||||
image_metadata['protected'] = (
|
|
||||||
utils.get_bool_param('protected', image_metadata))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = self.volume_api.copy_volume_to_image(context,
|
response = self.volume_api.copy_volume_to_image(context,
|
||||||
@ -295,6 +258,7 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-extend')
|
@wsgi.action('os-extend')
|
||||||
|
@validation.schema(volume_action.extend)
|
||||||
def _extend(self, req, id, body):
|
def _extend(self, req, id, body):
|
||||||
"""Extend size of volume."""
|
"""Extend size of volume."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
@ -302,12 +266,7 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
|
|
||||||
try:
|
|
||||||
size = int(body['os-extend']['new_size'])
|
size = int(body['os-extend']['new_size'])
|
||||||
except (KeyError, ValueError, TypeError):
|
|
||||||
msg = _("New volume size must be specified as an integer.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if (req_version.matches(mv.VOLUME_EXTEND_INUSE) and
|
if (req_version.matches(mv.VOLUME_EXTEND_INUSE) and
|
||||||
volume.status in ['in-use']):
|
volume.status in ['in-use']):
|
||||||
@ -319,64 +278,43 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-update_readonly_flag')
|
@wsgi.action('os-update_readonly_flag')
|
||||||
|
@validation.schema(volume_action.volume_readonly_update)
|
||||||
def _volume_readonly_update(self, req, id, body):
|
def _volume_readonly_update(self, req, id, body):
|
||||||
"""Update volume readonly flag."""
|
"""Update volume readonly flag."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
|
|
||||||
try:
|
|
||||||
readonly_flag = body['os-update_readonly_flag']['readonly']
|
readonly_flag = body['os-update_readonly_flag']['readonly']
|
||||||
except KeyError:
|
|
||||||
msg = _("Must specify readonly in request.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
readonly_flag = strutils.bool_from_string(readonly_flag,
|
readonly_flag = strutils.bool_from_string(readonly_flag,
|
||||||
strict=True)
|
strict=True)
|
||||||
except ValueError as error:
|
|
||||||
err_msg = encodeutils.exception_to_unicode(error)
|
|
||||||
msg = _("Invalid value for 'readonly': '%s'") % err_msg
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
self.volume_api.update_readonly_flag(context, volume, readonly_flag)
|
self.volume_api.update_readonly_flag(context, volume, readonly_flag)
|
||||||
|
|
||||||
@wsgi.response(http_client.ACCEPTED)
|
@wsgi.response(http_client.ACCEPTED)
|
||||||
@wsgi.action('os-retype')
|
@wsgi.action('os-retype')
|
||||||
|
@validation.schema(volume_action.retype)
|
||||||
def _retype(self, req, id, body):
|
def _retype(self, req, id, body):
|
||||||
"""Change type of existing volume."""
|
"""Change type of existing volume."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
try:
|
|
||||||
new_type = body['os-retype']['new_type']
|
new_type = body['os-retype']['new_type']
|
||||||
except KeyError:
|
|
||||||
msg = _("New volume type must be specified.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
policy = body['os-retype'].get('migration_policy')
|
policy = body['os-retype'].get('migration_policy')
|
||||||
|
|
||||||
self.volume_api.retype(context, volume, new_type, policy)
|
self.volume_api.retype(context, volume, new_type, policy)
|
||||||
|
|
||||||
@wsgi.response(http_client.OK)
|
@wsgi.response(http_client.OK)
|
||||||
@wsgi.action('os-set_bootable')
|
@wsgi.action('os-set_bootable')
|
||||||
|
@validation.schema(volume_action.set_bootable)
|
||||||
def _set_bootable(self, req, id, body):
|
def _set_bootable(self, req, id, body):
|
||||||
"""Update bootable status of a volume."""
|
"""Update bootable status of a volume."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
# Not found exception will be handled at the wsgi level
|
# Not found exception will be handled at the wsgi level
|
||||||
volume = self.volume_api.get(context, id)
|
volume = self.volume_api.get(context, id)
|
||||||
|
|
||||||
try:
|
bootable = strutils.bool_from_string(
|
||||||
bootable = body['os-set_bootable']['bootable']
|
body['os-set_bootable']['bootable'], strict=True)
|
||||||
except KeyError:
|
|
||||||
msg = _("Must specify bootable in request.")
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
try:
|
|
||||||
bootable = strutils.bool_from_string(bootable,
|
|
||||||
strict=True)
|
|
||||||
except ValueError as error:
|
|
||||||
err_msg = encodeutils.exception_to_unicode(error)
|
|
||||||
msg = _("Invalid value for 'bootable': '%s'") % err_msg
|
|
||||||
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
||||||
|
|
||||||
update_dict = {'bootable': bootable}
|
update_dict = {'bootable': bootable}
|
||||||
|
|
||||||
|
203
cinder/api/schemas/volume_actions.py
Normal file
203
cinder/api/schemas/volume_actions.py
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
# Copyright (C) 2018 NTT DATA
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Schema for V3 volume_actions API.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
from cinder.api.validation import parameter_types
|
||||||
|
|
||||||
|
|
||||||
|
container_format = parameter_types.description
|
||||||
|
|
||||||
|
extend = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-extend': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'new_size': parameter_types.volume_size,
|
||||||
|
},
|
||||||
|
'required': ['new_size'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-extend'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
attach = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-attach': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'instance_uuid': parameter_types.uuid,
|
||||||
|
'mountpoint': {
|
||||||
|
'type': 'string', 'minLength': 1,
|
||||||
|
'maxLength': 255
|
||||||
|
},
|
||||||
|
'host_name': {'type': 'string', 'maxLength': 255},
|
||||||
|
'mode': {'type': 'string', 'enum': ['rw', 'ro']}
|
||||||
|
},
|
||||||
|
'required': ['mountpoint'],
|
||||||
|
'anyOf': [{'required': ['instance_uuid']},
|
||||||
|
{'required': ['host_name']}],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-attach'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
detach = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-detach': {
|
||||||
|
'type': ['object', 'null'],
|
||||||
|
'properties': {
|
||||||
|
'attachment_id': parameter_types.uuid,
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-detach'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
retype = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-retype': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'new_type': {'type': 'string'},
|
||||||
|
'migration_policy': {
|
||||||
|
'type': ['string', 'null'],
|
||||||
|
'enum': ['on-demand', 'never']},
|
||||||
|
},
|
||||||
|
'required': ['new_type'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-retype'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
set_bootable = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-set_bootable': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'bootable': parameter_types.boolean
|
||||||
|
},
|
||||||
|
'required': ['bootable'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-set_bootable'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
volume_upload_image = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-volume_upload_image': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'image_name': {
|
||||||
|
'type': 'string', 'minLength': 1, 'maxLength': 255
|
||||||
|
},
|
||||||
|
'force': parameter_types.boolean,
|
||||||
|
'disk_format': {
|
||||||
|
'type': 'string',
|
||||||
|
'enum': ['raw', 'vmdk', 'vdi', 'qcow2',
|
||||||
|
'vhd', 'vhdx', 'ploop']
|
||||||
|
},
|
||||||
|
'container_format': container_format
|
||||||
|
},
|
||||||
|
'required': ['image_name'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-volume_upload_image'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_upload_image_v31 = copy.deepcopy(volume_upload_image)
|
||||||
|
volume_upload_image_v31['properties']['os-volume_upload_image']['properties'][
|
||||||
|
'visibility'] = {'type': 'string',
|
||||||
|
'enum': ['community', 'public', 'private', 'shared']}
|
||||||
|
volume_upload_image_v31['properties']['os-volume_upload_image']['properties'][
|
||||||
|
'protected'] = parameter_types.boolean
|
||||||
|
|
||||||
|
|
||||||
|
initialize_connection = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-initialize_connection': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'connector': {'type': ['object', 'string']},
|
||||||
|
},
|
||||||
|
'required': ['connector'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-initialize_connection'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
terminate_connection = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-terminate_connection': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'connector': {'type': ['string', 'object', 'null']},
|
||||||
|
},
|
||||||
|
'required': ['connector'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-terminate_connection'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_readonly_update = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-update_readonly_flag': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'readonly': parameter_types.boolean
|
||||||
|
},
|
||||||
|
'required': ['readonly'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-update_readonly_flag'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
@ -201,3 +201,10 @@ backup_service = {'type': 'string', 'minLength': 0, 'maxLength': 255}
|
|||||||
nullable_string = {
|
nullable_string = {
|
||||||
'type': ('string', 'null'), 'minLength': 0, 'maxLength': 255
|
'type': ('string', 'null'), 'minLength': 0, 'maxLength': 255
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
volume_size = {
|
||||||
|
'type': ['integer', 'string'],
|
||||||
|
'pattern': '^[0-9]+$',
|
||||||
|
'minimum': 1
|
||||||
|
}
|
||||||
|
@ -60,8 +60,6 @@ QEMU_IMG_LIMITS = processutils.ProcessLimits(
|
|||||||
cpu_time=8,
|
cpu_time=8,
|
||||||
address_space=1 * units.Gi)
|
address_space=1 * units.Gi)
|
||||||
|
|
||||||
VALID_DISK_FORMATS = ('raw', 'vmdk', 'vdi', 'qcow2',
|
|
||||||
'vhd', 'vhdx', 'ploop')
|
|
||||||
|
|
||||||
QEMU_IMG_FORMAT_MAP = {
|
QEMU_IMG_FORMAT_MAP = {
|
||||||
# Convert formats of Glance images to how they are processed with qemu-img.
|
# Convert formats of Glance images to how they are processed with qemu-img.
|
||||||
@ -76,10 +74,6 @@ QEMU_IMG_MIN_FORCE_SHARE_VERSION = [2, 10, 0]
|
|||||||
QEMU_IMG_MIN_CONVERT_LUKS_VERSION = '2.10'
|
QEMU_IMG_MIN_CONVERT_LUKS_VERSION = '2.10'
|
||||||
|
|
||||||
|
|
||||||
def validate_disk_format(disk_format):
|
|
||||||
return disk_format in VALID_DISK_FORMATS
|
|
||||||
|
|
||||||
|
|
||||||
def fixup_disk_format(disk_format):
|
def fixup_disk_format(disk_format):
|
||||||
"""Return the format to be provided to qemu-img convert."""
|
"""Return the format to be provided to qemu-img convert."""
|
||||||
|
|
||||||
|
@ -20,11 +20,13 @@ import mock
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
import oslo_messaging as messaging
|
import oslo_messaging as messaging
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
|
import six
|
||||||
from six.moves import http_client
|
from six.moves import http_client
|
||||||
import webob
|
import webob
|
||||||
|
|
||||||
from cinder.api.contrib import volume_actions
|
from cinder.api.contrib import volume_actions
|
||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
|
from cinder.api.openstack import api_version_request as api_version
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import db
|
from cinder import db
|
||||||
from cinder import exception
|
from cinder import exception
|
||||||
@ -55,7 +57,7 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
super(VolumeActionsTest, self).setUp()
|
super(VolumeActionsTest, self).setUp()
|
||||||
self.context = context.RequestContext(fake.USER_ID, fake.PROJECT_ID,
|
self.context = context.RequestContext(fake.USER_ID, fake.PROJECT_ID,
|
||||||
is_admin=False)
|
is_admin=False)
|
||||||
self.UUID = uuid.uuid4()
|
self.UUID = six.text_type(uuid.uuid4())
|
||||||
self.controller = volume_actions.VolumeActionsController()
|
self.controller = volume_actions.VolumeActionsController()
|
||||||
self.api_patchers = {}
|
self.api_patchers = {}
|
||||||
for _meth in self._methods:
|
for _meth in self._methods:
|
||||||
@ -101,7 +103,8 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
with mock.patch.object(volume_api.API,
|
with mock.patch.object(volume_api.API,
|
||||||
'initialize_connection') as init_conn:
|
'initialize_connection') as init_conn:
|
||||||
init_conn.return_value = {}
|
init_conn.return_value = {}
|
||||||
body = {'os-initialize_connection': {'connector': 'fake'}}
|
body = {'os-initialize_connection': {'connector': {
|
||||||
|
'fake': 'fake'}}}
|
||||||
req = webob.Request.blank('/v2/%s/volumes/%s/action' %
|
req = webob.Request.blank('/v2/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, fake.VOLUME_ID))
|
(fake.PROJECT_ID, fake.VOLUME_ID))
|
||||||
req.method = "POST"
|
req.method = "POST"
|
||||||
@ -147,7 +150,8 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
'initialize_connection') as init_conn:
|
'initialize_connection') as init_conn:
|
||||||
init_conn.side_effect = \
|
init_conn.side_effect = \
|
||||||
exception.VolumeBackendAPIException(data=None)
|
exception.VolumeBackendAPIException(data=None)
|
||||||
body = {'os-initialize_connection': {'connector': 'fake'}}
|
body = {'os-initialize_connection': {'connector': {
|
||||||
|
'fake': 'fake'}}}
|
||||||
req = webob.Request.blank('/v2/%s/volumes/%s/action' %
|
req = webob.Request.blank('/v2/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, fake.VOLUME_ID))
|
(fake.PROJECT_ID, fake.VOLUME_ID))
|
||||||
req.method = "POST"
|
req.method = "POST"
|
||||||
@ -262,7 +266,7 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
self.controller._attach,
|
self.controller._attach,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
def test_volume_attach_to_instance_raises_db_error(self):
|
def test_volume_attach_to_instance_raises_db_error(self):
|
||||||
# In case of DB error 500 error code is returned to user
|
# In case of DB error 500 error code is returned to user
|
||||||
@ -281,7 +285,7 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
self.controller._attach,
|
self.controller._attach,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
def test_detach(self):
|
def test_detach(self):
|
||||||
body = {'os-detach': {'attachment_id': fake.ATTACHMENT_ID}}
|
body = {'os-detach': {'attachment_id': fake.ATTACHMENT_ID}}
|
||||||
@ -309,7 +313,7 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
self.controller._detach,
|
self.controller._detach,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
def test_volume_detach_raises_db_error(self):
|
def test_volume_detach_raises_db_error(self):
|
||||||
# In case of DB error 500 error code is returned to user
|
# In case of DB error 500 error code is returned to user
|
||||||
@ -326,7 +330,7 @@ class VolumeActionsTest(test.TestCase):
|
|||||||
self.controller._detach,
|
self.controller._detach,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
def test_attach_with_invalid_arguments(self):
|
def test_attach_with_invalid_arguments(self):
|
||||||
# Invalid request to attach volume an invalid target
|
# Invalid request to attach volume an invalid target
|
||||||
@ -793,7 +797,6 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
vol = {
|
vol = {
|
||||||
"container_format": 'bare',
|
"container_format": 'bare',
|
||||||
"disk_format": 'raw',
|
"disk_format": 'raw',
|
||||||
"updated_at": datetime.datetime(1, 1, 1, 1, 1, 1),
|
|
||||||
"image_name": 'image_name',
|
"image_name": 'image_name',
|
||||||
"force": True}
|
"force": True}
|
||||||
body = {"os-volume_upload_image": vol}
|
body = {"os-volume_upload_image": vol}
|
||||||
@ -853,7 +856,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = {"os-volume_upload_image": img}
|
body = {"os-volume_upload_image": img}
|
||||||
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, id))
|
(fake.PROJECT_ID, id))
|
||||||
res_dict = self.controller._volume_upload_image(req, id, body)
|
res_dict = self.controller._volume_upload_image(req, id, body=body)
|
||||||
expected = {'os-volume_upload_image':
|
expected = {'os-volume_upload_image':
|
||||||
{'id': id,
|
{'id': id,
|
||||||
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
'updated_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
||||||
@ -887,7 +890,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
|
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
|
||||||
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
|
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
|
||||||
@ -905,7 +908,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
|
@mock.patch.object(volume_api.API, 'get', fake_volume_get)
|
||||||
def test_copy_volume_to_image_invalid_disk_format(self):
|
def test_copy_volume_to_image_invalid_disk_format(self):
|
||||||
@ -917,11 +920,11 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = {"os-volume_upload_image": vol}
|
body = {"os-volume_upload_image": vol}
|
||||||
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action'
|
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action'
|
||||||
% (fake.PROJECT_ID, id))
|
% (fake.PROJECT_ID, id))
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(exception.ValidationError,
|
||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, "copy_volume_to_image")
|
@mock.patch.object(volume_api.API, "copy_volume_to_image")
|
||||||
def test_copy_volume_to_image_disk_format_ploop(self,
|
def test_copy_volume_to_image_disk_format_ploop(self,
|
||||||
@ -938,7 +941,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
image_metadata = {'container_format': 'bare',
|
image_metadata = {'container_format': 'bare',
|
||||||
'disk_format': 'ploop',
|
'disk_format': 'ploop',
|
||||||
'name': 'image_name'}
|
'name': 'image_name'}
|
||||||
self.controller._volume_upload_image(req, volume.id, body)
|
self.controller._volume_upload_image(req, volume.id, body=body)
|
||||||
|
|
||||||
mock_copy_to_image.assert_called_once_with(
|
mock_copy_to_image.assert_called_once_with(
|
||||||
req.environ['cinder.context'], volume, image_metadata, False)
|
req.environ['cinder.context'], volume, image_metadata, False)
|
||||||
@ -959,7 +962,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
|
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
|
||||||
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
|
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
|
||||||
@ -977,7 +980,35 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
|
@mock.patch.object(volume_api.API, 'get', fake_volume_get_obj)
|
||||||
|
@mock.patch.object(volume_api.API, 'copy_volume_to_image',
|
||||||
|
side_effect=messaging.RemoteError)
|
||||||
|
@ddt.data(
|
||||||
|
({"image_name": 'image_name', "protected": None},
|
||||||
|
exception.ValidationError),
|
||||||
|
({"image_name": 'image_name', "protected": ' '},
|
||||||
|
exception.ValidationError),
|
||||||
|
({"image_name": 'image_name', "protected": 'test'},
|
||||||
|
exception.ValidationError),
|
||||||
|
({"image_name": 'image_name', "visibility": 'test'},
|
||||||
|
exception.ValidationError),
|
||||||
|
({"image_name": 'image_name', "visibility": ' '},
|
||||||
|
exception.ValidationError),
|
||||||
|
({"image_name": 'image_name', "visibility": None},
|
||||||
|
exception.ValidationError))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_copy_volume_to_image_invalid_request_body(
|
||||||
|
self, vol, exception, mock_copy):
|
||||||
|
id = fake.VOLUME2_ID
|
||||||
|
body = {"os-volume_upload_image": vol}
|
||||||
|
req = fakes.HTTPRequest.blank('/v3/%s/volumes/%s/action' %
|
||||||
|
(fake.PROJECT_ID, id))
|
||||||
|
req.api_version_request = api_version.APIVersionRequest("3.1")
|
||||||
|
self.assertRaises(exception,
|
||||||
|
self.controller._volume_upload_image,
|
||||||
|
req, id, body=body)
|
||||||
|
|
||||||
def test_volume_upload_image_typeerror(self):
|
def test_volume_upload_image_typeerror(self):
|
||||||
id = fake.VOLUME2_ID
|
id = fake.VOLUME2_ID
|
||||||
@ -1011,11 +1042,11 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = {'os-extend': {'new_size': 'fake'}}
|
body = {'os-extend': {'new_size': 'fake'}}
|
||||||
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, id))
|
(fake.PROJECT_ID, id))
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(exception.ValidationError,
|
||||||
self.controller._extend,
|
self.controller._extend,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
@ddt.data({'version': mv.get_prior_version(mv.VOLUME_EXTEND_INUSE),
|
@ddt.data({'version': mv.get_prior_version(mv.VOLUME_EXTEND_INUSE),
|
||||||
'status': 'available'},
|
'status': 'available'},
|
||||||
@ -1036,7 +1067,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
req = fakes.HTTPRequest.blank('/v3/%s/volumes/%s/action' %
|
req = fakes.HTTPRequest.blank('/v3/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, vol['id']))
|
(fake.PROJECT_ID, vol['id']))
|
||||||
req.api_version_request = mv.get_api_version(version)
|
req.api_version_request = mv.get_api_version(version)
|
||||||
self.controller._extend(req, vol['id'], body)
|
self.controller._extend(req, vol['id'], body=body)
|
||||||
if version == mv.VOLUME_EXTEND_INUSE and status == 'in-use':
|
if version == mv.VOLUME_EXTEND_INUSE and status == 'in-use':
|
||||||
mock_extend.assert_called_with(req.environ['cinder.context'],
|
mock_extend.assert_called_with(req.environ['cinder.context'],
|
||||||
vol, 2, attached=True)
|
vol, 2, attached=True)
|
||||||
@ -1053,11 +1084,11 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = {"os-volume_upload_image": vol}
|
body = {"os-volume_upload_image": vol}
|
||||||
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
req = fakes.HTTPRequest.blank('/v2/%s/volumes/%s/action' %
|
||||||
(fake.PROJECT_ID, id))
|
(fake.PROJECT_ID, id))
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(exception.ValidationError,
|
||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req,
|
req,
|
||||||
id,
|
id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
def _create_volume_with_type(self, status='available',
|
def _create_volume_with_type(self, status='available',
|
||||||
display_description='displaydesc', **kwargs):
|
display_description='displaydesc', **kwargs):
|
||||||
@ -1104,7 +1135,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
use_admin_context=self.context.is_admin)
|
use_admin_context=self.context.is_admin)
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
|
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1123,7 +1155,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body['os-volume_upload_image']['visibility'] = 'public'
|
body['os-volume_upload_image']['visibility'] = 'public'
|
||||||
self.assertRaises(exception.PolicyNotAuthorized,
|
self.assertRaises(exception.PolicyNotAuthorized,
|
||||||
self.controller._volume_upload_image,
|
self.controller._volume_upload_image,
|
||||||
req, id, body)
|
req, id, body=body)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, "get_volume_image_metadata")
|
@mock.patch.object(volume_api.API, "get_volume_image_metadata")
|
||||||
@mock.patch.object(glance.GlanceImageService, "create")
|
@mock.patch.object(glance.GlanceImageService, "create")
|
||||||
@ -1145,7 +1177,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
'/v2/%s/volumes/%s/action' % (fake.PROJECT_ID, volume.id),
|
'/v2/%s/volumes/%s/action' % (fake.PROJECT_ID, volume.id),
|
||||||
use_admin_context=self.context.is_admin)
|
use_admin_context=self.context.is_admin)
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1171,7 +1204,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._volume_upload_image, req, volume.id,
|
self.controller._volume_upload_image, req, volume.id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
self.assertFalse(mock_copy_to_image.called)
|
self.assertFalse(mock_copy_to_image.called)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1199,7 +1232,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body['os-volume_upload_image']['force'] = False
|
body['os-volume_upload_image']['force'] = False
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._volume_upload_image, req, volume.id,
|
self.controller._volume_upload_image, req, volume.id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
self.assertFalse(mock_copy_to_image.called)
|
self.assertFalse(mock_copy_to_image.called)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1227,7 +1260,7 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
self.assertRaises(webob.exc.HTTPBadRequest,
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
self.controller._volume_upload_image, req, volume.id,
|
self.controller._volume_upload_image, req, volume.id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
self.assertFalse(mock_copy_to_image.called)
|
self.assertFalse(mock_copy_to_image.called)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1235,7 +1268,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
self.assertIsNone(vol_db.previous_status)
|
self.assertIsNone(vol_db.previous_status)
|
||||||
|
|
||||||
CONF.set_default('enable_force_upload', True)
|
CONF.set_default('enable_force_upload', True)
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
|
|
||||||
@ -1259,7 +1293,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
use_admin_context=self.context.is_admin)
|
use_admin_context=self.context.is_admin)
|
||||||
|
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1281,7 +1316,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
use_admin_context=self.context.is_admin)
|
use_admin_context=self.context.is_admin)
|
||||||
|
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1305,7 +1341,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
'/v2/%s/volumes/%s/action' % (fake.PROJECT_ID, volume.id),
|
'/v2/%s/volumes/%s/action' % (fake.PROJECT_ID, volume.id),
|
||||||
use_admin_context=self.context.is_admin)
|
use_admin_context=self.context.is_admin)
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
|
|
||||||
@mock.patch.object(volume_api.API, "get_volume_image_metadata")
|
@mock.patch.object(volume_api.API, "get_volume_image_metadata")
|
||||||
@ -1334,11 +1371,12 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
req.headers = mv.get_mv_header(mv.UPLOAD_IMAGE_PARAMS)
|
req.headers = mv.get_mv_header(mv.UPLOAD_IMAGE_PARAMS)
|
||||||
req.api_version_request = mv.get_api_version(mv.UPLOAD_IMAGE_PARAMS)
|
req.api_version_request = mv.get_api_version(mv.UPLOAD_IMAGE_PARAMS)
|
||||||
body = self._get_os_volume_upload_image()
|
body = self._get_os_volume_upload_image()
|
||||||
|
body = self._get_os_volume_upload_image()
|
||||||
body['os-volume_upload_image']['visibility'] = 'public'
|
body['os-volume_upload_image']['visibility'] = 'public'
|
||||||
body['os-volume_upload_image']['protected'] = True
|
body['os-volume_upload_image']['protected'] = True
|
||||||
res_dict = self.controller._volume_upload_image(req,
|
res_dict = self.controller._volume_upload_image(req,
|
||||||
volume.id,
|
volume.id,
|
||||||
body)
|
body=body)
|
||||||
|
|
||||||
expected['os-volume_upload_image'].update(visibility='public',
|
expected['os-volume_upload_image'].update(visibility='public',
|
||||||
protected=True)
|
protected=True)
|
||||||
@ -1360,7 +1398,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body['os-volume_upload_image']['container_format'] = 'bare'
|
body['os-volume_upload_image']['container_format'] = 'bare'
|
||||||
body['os-volume_upload_image']['disk_format'] = 'vhd'
|
body['os-volume_upload_image']['disk_format'] = 'vhd'
|
||||||
|
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
@ -1383,7 +1422,8 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
body['os-volume_upload_image']['container_format'] = 'bare'
|
body['os-volume_upload_image']['container_format'] = 'bare'
|
||||||
body['os-volume_upload_image']['disk_format'] = 'vhdx'
|
body['os-volume_upload_image']['disk_format'] = 'vhdx'
|
||||||
|
|
||||||
res_dict = self.controller._volume_upload_image(req, volume.id, body)
|
res_dict = self.controller._volume_upload_image(req, volume.id,
|
||||||
|
body=body)
|
||||||
|
|
||||||
self.assertDictEqual(expected, res_dict)
|
self.assertDictEqual(expected, res_dict)
|
||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
|
@ -1614,11 +1614,6 @@ class API(base.Base):
|
|||||||
def retype(self, context, volume, new_type, migration_policy=None):
|
def retype(self, context, volume, new_type, migration_policy=None):
|
||||||
"""Attempt to modify the type associated with an existing volume."""
|
"""Attempt to modify the type associated with an existing volume."""
|
||||||
context.authorize(vol_action_policy.RETYPE_POLICY, target_obj=volume)
|
context.authorize(vol_action_policy.RETYPE_POLICY, target_obj=volume)
|
||||||
if migration_policy and migration_policy not in ('on-demand', 'never'):
|
|
||||||
msg = _('migration_policy must be \'on-demand\' or \'never\', '
|
|
||||||
'passed: %s') % new_type
|
|
||||||
LOG.error(msg)
|
|
||||||
raise exception.InvalidInput(reason=msg)
|
|
||||||
|
|
||||||
# Support specifying volume type by ID or name
|
# Support specifying volume type by ID or name
|
||||||
try:
|
try:
|
||||||
|
Loading…
Reference in New Issue
Block a user