text: #   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_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 wsgi
  from cinder.api import xmlutil
  from cinder import exception
  from cinder.i18n import _
  from cinder import utils
  from cinder import volume
  
  
  LOG = logging.getLogger(__name__)
  
  
  def authorize(context, action_name):
      action = 'volume_actions:%s' % action_name
      extensions.extension_authorizer('volume', action)(context)
  
  
  class VolumeToImageSerializer(xmlutil.TemplateBuilder):
      def construct(self):
          root = xmlutil.TemplateElement('os-volume_upload_image',
                                         selector='os-volume_upload_image')
          root.set('id')
          root.set('updated_at')
          root.set('status')
          root.set('display_description')
          root.set('size')
          root.set('volume_type')
          root.set('image_id')
          root.set('container_format')
          root.set('disk_format')
          root.set('image_name')
          return xmlutil.MasterTemplate(root, 1)
  
  
  class VolumeToImageDeserializer(wsgi.XMLDeserializer):
      """Deserializer to handle xml-formatted requests."""
      def default(self, string):
          dom = utils.safe_minidom_parse_string(string)
          action_node = dom.childNodes[0]
          action_name = action_node.tagName
  
          action_data = {}
          attributes = ["force", "image_name", "container_format", "disk_format"]
          for attr in attributes:
              if action_node.hasAttribute(attr):
                  action_data[attr] = action_node.getAttribute(attr)
          if 'force' in action_data and action_data['force'] == 'True':
              action_data['force'] = True
          return {'body': {action_name: action_data}}
  
  
  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')
      @wsgi.serializers(xml=VolumeToImageSerializer)
      @wsgi.deserializers(xml=VolumeToImageDeserializer)
      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']
          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"]}
          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"
      namespace = "http://docs.openstack.org/volume/ext/volume-actions/api/v1.1"
      updated = "2012-05-31T00:00:00+00:00"
  
      def get_controller_extensions(self):
          controller = VolumeActionsController()
          extension = extensions.ControllerExtension(self, 'volumes', controller)
          return [extension]
  