c042a05ac3
Cinder XML API is not tested by Tempest about for one year. We don't know if it works or not. It was deprecated in Mitaka release. Implements blueprint: remove-xml-api APIImpact DocImpact UpgradeImpact Change-Id: If98db25f1f4032725444c3959acb9aad8f869802
385 lines
15 KiB
Python
385 lines
15 KiB
Python
# Copyright 2012 OpenStack Foundation
|
|
#
|
|
# 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.
|
|
|
|
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
import oslo_messaging as messaging
|
|
from oslo_utils import encodeutils
|
|
from oslo_utils import strutils
|
|
import six
|
|
import webob
|
|
|
|
from cinder.api import extensions
|
|
from cinder.api.openstack import api_version_request
|
|
from cinder.api.openstack import wsgi
|
|
from cinder import exception
|
|
from cinder.i18n import _
|
|
from cinder import utils
|
|
from cinder import volume
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
CONF = cfg.CONF
|
|
|
|
|
|
def authorize(context, action_name):
|
|
action = 'volume_actions:%s' % action_name
|
|
extensions.extension_authorizer('volume', action)(context)
|
|
|
|
|
|
class VolumeActionsController(wsgi.Controller):
|
|
def __init__(self, *args, **kwargs):
|
|
super(VolumeActionsController, self).__init__(*args, **kwargs)
|
|
self.volume_api = volume.API()
|
|
|
|
@wsgi.action('os-attach')
|
|
def _attach(self, req, id, body):
|
|
"""Add attachment metadata."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
# instance uuid is an option now
|
|
instance_uuid = None
|
|
if 'instance_uuid' in body['os-attach']:
|
|
instance_uuid = body['os-attach']['instance_uuid']
|
|
host_name = None
|
|
# Keep API backward compatibility
|
|
if 'host_name' in body['os-attach']:
|
|
host_name = body['os-attach']['host_name']
|
|
mountpoint = body['os-attach']['mountpoint']
|
|
if 'mode' in body['os-attach']:
|
|
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:
|
|
self.volume_api.attach(context, volume,
|
|
instance_uuid, host_name, mountpoint, mode)
|
|
except messaging.RemoteError as error:
|
|
if error.exc_type in ['InvalidVolume', 'InvalidUUID',
|
|
'InvalidVolumeAttachMode']:
|
|
msg = "Error attaching volume - %(err_type)s: %(err_msg)s" % {
|
|
'err_type': error.exc_type, 'err_msg': error.value}
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
else:
|
|
# There are also few cases where attach call could fail due to
|
|
# db or volume driver errors. These errors shouldn't be exposed
|
|
# to the user and in such cases it should raise 500 error.
|
|
raise
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-detach')
|
|
def _detach(self, req, id, body):
|
|
"""Clear attachment metadata."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
attachment_id = None
|
|
if body['os-detach']:
|
|
attachment_id = body['os-detach'].get('attachment_id', None)
|
|
|
|
try:
|
|
self.volume_api.detach(context, volume, attachment_id)
|
|
except messaging.RemoteError as error:
|
|
if error.exc_type in ['VolumeAttachmentNotFound', 'InvalidVolume']:
|
|
msg = "Error detaching volume - %(err_type)s: %(err_msg)s" % \
|
|
{'err_type': error.exc_type, 'err_msg': error.value}
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
else:
|
|
# There are also few cases where detach call could fail due to
|
|
# db or volume driver errors. These errors shouldn't be exposed
|
|
# to the user and in such cases it should raise 500 error.
|
|
raise
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-reserve')
|
|
def _reserve(self, req, id, body):
|
|
"""Mark volume as reserved."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
self.volume_api.reserve_volume(context, volume)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-unreserve')
|
|
def _unreserve(self, req, id, body):
|
|
"""Unmark volume as reserved."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
self.volume_api.unreserve_volume(context, volume)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-begin_detaching')
|
|
def _begin_detaching(self, req, id, body):
|
|
"""Update volume status to 'detaching'."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
self.volume_api.begin_detaching(context, volume)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-roll_detaching')
|
|
def _roll_detaching(self, req, id, body):
|
|
"""Roll back volume status to 'in-use'."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
self.volume_api.roll_detaching(context, volume)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-initialize_connection')
|
|
def _initialize_connection(self, req, id, body):
|
|
"""Initialize volume attachment."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
try:
|
|
connector = body['os-initialize_connection']['connector']
|
|
except KeyError:
|
|
raise webob.exc.HTTPBadRequest(
|
|
explanation=_("Must specify 'connector'"))
|
|
try:
|
|
info = self.volume_api.initialize_connection(context,
|
|
volume,
|
|
connector)
|
|
except exception.InvalidInput as err:
|
|
raise webob.exc.HTTPBadRequest(
|
|
explanation=err)
|
|
except exception.VolumeBackendAPIException as error:
|
|
msg = _("Unable to fetch connection information from backend.")
|
|
raise webob.exc.HTTPInternalServerError(explanation=msg)
|
|
|
|
return {'connection_info': info}
|
|
|
|
@wsgi.action('os-terminate_connection')
|
|
def _terminate_connection(self, req, id, body):
|
|
"""Terminate volume attachment."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
try:
|
|
connector = body['os-terminate_connection']['connector']
|
|
except KeyError:
|
|
raise webob.exc.HTTPBadRequest(
|
|
explanation=_("Must specify 'connector'"))
|
|
try:
|
|
self.volume_api.terminate_connection(context, volume, connector)
|
|
except exception.VolumeBackendAPIException as error:
|
|
msg = _("Unable to terminate volume connection from backend.")
|
|
raise webob.exc.HTTPInternalServerError(explanation=msg)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.response(202)
|
|
@wsgi.action('os-volume_upload_image')
|
|
def _volume_upload_image(self, req, id, body):
|
|
"""Uploads the specified volume to image service."""
|
|
context = req.environ['cinder.context']
|
|
params = body['os-volume_upload_image']
|
|
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')
|
|
try:
|
|
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)
|
|
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
authorize(context, "upload_image")
|
|
image_metadata = {"container_format": params.get("container_format",
|
|
"bare"),
|
|
"disk_format": params.get("disk_format", "raw"),
|
|
"name": params["image_name"]}
|
|
|
|
if req_version >= api_version_request.APIVersionRequest('3.1'):
|
|
|
|
image_metadata['visibility'] = params.get('visibility', 'private')
|
|
image_metadata['protected'] = params.get('protected', 'False')
|
|
|
|
if image_metadata['visibility'] == 'public':
|
|
authorize(context, 'upload_public')
|
|
|
|
if CONF.glance_api_version != 2:
|
|
# Replace visibility with is_public for Glance V1
|
|
image_metadata['is_public'] = (
|
|
image_metadata['visibility'] == 'public')
|
|
image_metadata.pop('visibility', None)
|
|
|
|
image_metadata['protected'] = (
|
|
utils.get_bool_param('protected', image_metadata))
|
|
|
|
try:
|
|
response = self.volume_api.copy_volume_to_image(context,
|
|
volume,
|
|
image_metadata,
|
|
force)
|
|
except exception.InvalidVolume as error:
|
|
raise webob.exc.HTTPBadRequest(explanation=error.msg)
|
|
except ValueError as error:
|
|
raise webob.exc.HTTPBadRequest(explanation=six.text_type(error))
|
|
except messaging.RemoteError as error:
|
|
msg = "%(err_type)s: %(err_msg)s" % {'err_type': error.exc_type,
|
|
'err_msg': error.value}
|
|
raise webob.exc.HTTPBadRequest(explanation=msg)
|
|
except Exception as error:
|
|
raise webob.exc.HTTPBadRequest(explanation=six.text_type(error))
|
|
return {'os-volume_upload_image': response}
|
|
|
|
@wsgi.action('os-extend')
|
|
def _extend(self, req, id, body):
|
|
"""Extend size of volume."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
try:
|
|
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)
|
|
|
|
size = int(body['os-extend']['new_size'])
|
|
try:
|
|
self.volume_api.extend(context, volume, size)
|
|
except exception.InvalidVolume as error:
|
|
raise webob.exc.HTTPBadRequest(explanation=error.msg)
|
|
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-update_readonly_flag')
|
|
def _volume_readonly_update(self, req, id, body):
|
|
"""Update volume readonly flag."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
try:
|
|
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,
|
|
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)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-retype')
|
|
def _retype(self, req, id, body):
|
|
"""Change type of existing volume."""
|
|
context = req.environ['cinder.context']
|
|
volume = self.volume_api.get(context, id)
|
|
try:
|
|
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')
|
|
|
|
self.volume_api.retype(context, volume, new_type, policy)
|
|
return webob.Response(status_int=202)
|
|
|
|
@wsgi.action('os-set_bootable')
|
|
def _set_bootable(self, req, id, body):
|
|
"""Update bootable status of a volume."""
|
|
context = req.environ['cinder.context']
|
|
try:
|
|
volume = self.volume_api.get(context, id)
|
|
except exception.VolumeNotFound as error:
|
|
raise webob.exc.HTTPNotFound(explanation=error.msg)
|
|
|
|
try:
|
|
bootable = body['os-set_bootable']['bootable']
|
|
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}
|
|
|
|
self.volume_api.update(context, volume, update_dict)
|
|
return webob.Response(status_int=200)
|
|
|
|
|
|
class Volume_actions(extensions.ExtensionDescriptor):
|
|
"""Enable volume actions."""
|
|
|
|
name = "VolumeActions"
|
|
alias = "os-volume-actions"
|
|
updated = "2012-05-31T00:00:00+00:00"
|
|
|
|
def get_controller_extensions(self):
|
|
controller = VolumeActionsController()
|
|
extension = extensions.ControllerExtension(self, 'volumes', controller)
|
|
return [extension]
|