text: # Copyright 2011 Justin Santa Barbara
  # 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.
  
  """The volumes extension."""
  
  from oslo_utils import strutils
  from webob import exc
  
  from nova.api.openstack import api_version_request
  from nova.api.openstack.api_version_request \
      import MAX_PROXY_API_SUPPORT_VERSION
  from nova.api.openstack import common
  from nova.api.openstack.compute.schemas import volumes as volumes_schema
  from nova.api.openstack import extensions
  from nova.api.openstack import wsgi
  from nova.api import validation
  from nova import compute
  from nova.compute import vm_states
  from nova import exception
  from nova.i18n import _
  from nova import objects
  from nova.policies import volumes as vol_policies
  from nova.policies import volumes_attachments as va_policies
  from nova.volume import cinder
  
  ALIAS = "os-volumes"
  
  
  def _translate_volume_detail_view(context, vol):
      """Maps keys for volumes details view."""
  
      d = _translate_volume_summary_view(context, vol)
  
      # No additional data / lookups at the moment
  
      return d
  
  
  def _translate_volume_summary_view(context, vol):
      """Maps keys for volumes summary view."""
      d = {}
  
      d['id'] = vol['id']
      d['status'] = vol['status']
      d['size'] = vol['size']
      d['availabilityZone'] = vol['availability_zone']
      d['createdAt'] = vol['created_at']
  
      if vol['attach_status'] == 'attached':
          # NOTE(ildikov): The attachments field in the volume info that
          # Cinder sends is converted to an OrderedDict with the
          # instance_uuid as key to make it easier for the multiattach
          # feature to check the required information. Multiattach will
          # be enable in the Nova API in Newton.
          # The format looks like the following:
          # attachments = {'instance_uuid': {
          #                   'attachment_id': 'attachment_uuid',
          #                   'mountpoint': '/dev/sda/
          #                    }
          #                }
          attachment = list(vol['attachments'].items())[0]
          d['attachments'] = [_translate_attachment_detail_view(vol['id'],
              attachment[0],
              attachment[1].get('mountpoint'))]
      else:
          d['attachments'] = [{}]
  
      d['displayName'] = vol['display_name']
      d['displayDescription'] = vol['display_description']
  
      if vol['volume_type_id'] and vol.get('volume_type'):
          d['volumeType'] = vol['volume_type']['name']
      else:
          d['volumeType'] = vol['volume_type_id']
  
      d['snapshotId'] = vol['snapshot_id']
  
      if vol.get('volume_metadata'):
          d['metadata'] = vol.get('volume_metadata')
      else:
          d['metadata'] = {}
  
      return d
  
  
  class VolumeController(wsgi.Controller):
      """The Volumes API controller for the OpenStack API."""
  
      def __init__(self):
          self.volume_api = cinder.API()
          super(VolumeController, self).__init__()
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(404)
      def show(self, req, id):
          """Return data about the given volume."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          try:
              vol = self.volume_api.get(context, id)
          except exception.VolumeNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
          return {'volume': _translate_volume_detail_view(context, vol)}
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @wsgi.response(202)
      @extensions.expected_errors((400, 404))
      def delete(self, req, id):
          """Delete a volume."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          try:
              self.volume_api.delete(context, id)
          except exception.InvalidInput as e:
              raise exc.HTTPBadRequest(explanation=e.format_message())
          except exception.VolumeNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(())
      def index(self, req):
          """Returns a summary list of volumes."""
          return self._items(req, entity_maker=_translate_volume_summary_view)
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(())
      def detail(self, req):
          """Returns a detailed list of volumes."""
          return self._items(req, entity_maker=_translate_volume_detail_view)
  
      def _items(self, req, entity_maker):
          """Returns a list of volumes, transformed through entity_maker."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          volumes = self.volume_api.get_all(context)
          limited_list = common.limited(volumes, req)
          res = [entity_maker(context, vol) for vol in limited_list]
          return {'volumes': res}
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors((400, 403, 404))
      @validation.schema(volumes_schema.create)
      def create(self, req, body):
          """Creates a new volume."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          vol = body['volume']
  
          vol_type = vol.get('volume_type')
          metadata = vol.get('metadata')
          snapshot_id = vol.get('snapshot_id', None)
  
          if snapshot_id is not None:
              try:
                  snapshot = self.volume_api.get_snapshot(context, snapshot_id)
              except exception.SnapshotNotFound as e:
                  raise exc.HTTPNotFound(explanation=e.format_message())
          else:
              snapshot = None
  
          size = vol.get('size', None)
          if size is None and snapshot is not None:
              size = snapshot['volume_size']
  
          availability_zone = vol.get('availability_zone')
  
          try:
              new_volume = self.volume_api.create(
                  context,
                  size,
                  vol.get('display_name'),
                  vol.get('display_description'),
                  snapshot=snapshot,
                  volume_type=vol_type,
                  metadata=metadata,
                  availability_zone=availability_zone
                  )
          except exception.InvalidInput as err:
              raise exc.HTTPBadRequest(explanation=err.format_message())
          except exception.OverQuota as err:
              raise exc.HTTPForbidden(explanation=err.format_message())
  
          # TODO(vish): Instance should be None at db layer instead of
          #             trying to lazy load, but for now we turn it into
          #             a dict to avoid an error.
          retval = _translate_volume_detail_view(context, dict(new_volume))
          result = {'volume': retval}
  
          location = '%s/%s' % (req.url, new_volume['id'])
  
          return wsgi.ResponseObject(result, headers=dict(location=location))
  
  
  def _translate_attachment_detail_view(volume_id, instance_uuid, mountpoint):
      """Maps keys for attachment details view."""
  
      d = _translate_attachment_summary_view(volume_id,
              instance_uuid,
              mountpoint)
  
      # No additional data / lookups at the moment
      return d
  
  
  def _translate_attachment_summary_view(volume_id, instance_uuid, mountpoint):
      """Maps keys for attachment summary view."""
      d = {}
  
      # NOTE(justinsb): We use the volume id as the id of the attachment object
      d['id'] = volume_id
  
      d['volumeId'] = volume_id
  
      d['serverId'] = instance_uuid
      if mountpoint:
          d['device'] = mountpoint
  
      return d
  
  
  def _check_request_version(req, min_version, method, server_id, server_state):
      if not api_version_request.is_supported(req, min_version=min_version):
          exc_inv = exception.InstanceInvalidState(
                  attr='vm_state',
                  instance_uuid=server_id,
                  state=server_state,
                  method=method)
          common.raise_http_conflict_for_instance_invalid_state(
                  exc_inv,
                  method,
                  server_id)
  
  
  class VolumeAttachmentController(wsgi.Controller):
      """The volume attachment API controller for the OpenStack API.
  
      A child resource of the server.  Note that we use the volume id
      as the ID of the attachment (though this is not guaranteed externally)
  
      """
  
      def __init__(self):
          self.compute_api = compute.API()
          self.volume_api = cinder.API()
          super(VolumeAttachmentController, self).__init__()
  
      @extensions.expected_errors(404)
      def index(self, req, server_id):
          """Returns the list of volume attachments for a given instance."""
          context = req.environ['nova.context']
          context.can(va_policies.POLICY_ROOT % 'index')
  
          instance = common.get_instance(self.compute_api, context, server_id)
  
          bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
                  context, instance.uuid)
          limited_list = common.limited(bdms, req)
  
          results = []
          for bdm in limited_list:
              if bdm.volume_id:
                  va = _translate_attachment_summary_view(bdm.volume_id,
                                                          bdm.instance_uuid,
                                                          bdm.device_name)
                  results.append(va)
  
          return {'volumeAttachments': results}
  
      @extensions.expected_errors(404)
      def show(self, req, server_id, id):
          """Return data about the given volume attachment."""
          context = req.environ['nova.context']
          context.can(va_policies.POLICY_ROOT % 'show')
  
          volume_id = id
          instance = common.get_instance(self.compute_api, context, server_id)
  
          bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
                  context, instance.uuid)
  
          if not bdms:
              msg = _("Instance %s is not attached.") % server_id
              raise exc.HTTPNotFound(explanation=msg)
  
          assigned_mountpoint = None
  
          for bdm in bdms:
              if bdm.volume_id == volume_id:
                  assigned_mountpoint = bdm.device_name
                  break
  
          if assigned_mountpoint is None:
              msg = _("volume_id not found: %s") % volume_id
              raise exc.HTTPNotFound(explanation=msg)
  
          return {'volumeAttachment': _translate_attachment_detail_view(
              volume_id,
              instance.uuid,
              assigned_mountpoint)}
  
      # TODO(mriedem): This API should return a 202 instead of a 200 response.
      @extensions.expected_errors((400, 404, 409))
      @validation.schema(volumes_schema.create_volume_attachment)
      def create(self, req, server_id, body):
          """Attach a volume to an instance."""
          context = req.environ['nova.context']
          context.can(va_policies.POLICY_ROOT % 'create')
  
          volume_id = body['volumeAttachment']['volumeId']
          device = body['volumeAttachment'].get('device')
  
          instance = common.get_instance(self.compute_api, context, server_id)
  
          if instance.vm_state in (vm_states.SHELVED,
                                   vm_states.SHELVED_OFFLOADED):
              _check_request_version(req, '2.20', 'attach_volume',
                                     server_id, instance.vm_state)
  
          try:
              device = self.compute_api.attach_volume(context, instance,
                                                      volume_id, device)
          except exception.InstanceUnknownCell as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
          except exception.VolumeNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
          except exception.InstanceIsLocked as e:
              raise exc.HTTPConflict(explanation=e.format_message())
          except exception.DevicePathInUse as e:
              raise exc.HTTPConflict(explanation=e.format_message())
          except exception.InstanceInvalidState as state_error:
              common.raise_http_conflict_for_instance_invalid_state(state_error,
                      'attach_volume', server_id)
          except (exception.InvalidVolume,
                  exception.InvalidDevicePath, exception.InvalidInput) as e:
              raise exc.HTTPBadRequest(explanation=e.format_message())
  
          # The attach is async
          attachment = {}
          attachment['id'] = volume_id
          attachment['serverId'] = server_id
          attachment['volumeId'] = volume_id
          attachment['device'] = device
  
          # NOTE(justinsb): And now, we have a problem...
          # The attach is async, so there's a window in which we don't see
          # the attachment (until the attachment completes).  We could also
          # get problems with concurrent requests.  I think we need an
          # attachment state, and to write to the DB here, but that's a bigger
          # change.
          # For now, we'll probably have to rely on libraries being smart
  
          # TODO(justinsb): How do I return "accepted" here?
          return {'volumeAttachment': attachment}
  
      @wsgi.response(202)
      @extensions.expected_errors((400, 404, 409))
      @validation.schema(volumes_schema.update_volume_attachment)
      def update(self, req, server_id, id, body):
          context = req.environ['nova.context']
          context.can(va_policies.POLICY_ROOT % 'update')
  
          old_volume_id = id
          try:
              old_volume = self.volume_api.get(context, old_volume_id)
          except exception.VolumeNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
          new_volume_id = body['volumeAttachment']['volumeId']
          try:
              new_volume = self.volume_api.get(context, new_volume_id)
          except exception.VolumeNotFound as e:
              # NOTE: This BadRequest is different from the above NotFound even
              # though the same VolumeNotFound exception. This is intentional
              # because new_volume_id is specified in a request body and if a
              # nonexistent resource in the body (not URI) the code should be
              # 400 Bad Request as API-WG guideline. On the other hand,
              # old_volume_id is specified with URI. So it is valid to return
              # NotFound response if that is not existent.
              raise exc.HTTPBadRequest(explanation=e.format_message())
  
          instance = common.get_instance(self.compute_api, context, server_id)
  
          bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
                  context, instance.uuid)
          found = False
          try:
              for bdm in bdms:
                  if bdm.volume_id != old_volume_id:
                      continue
                  try:
                      self.compute_api.swap_volume(context, instance, old_volume,
                                                   new_volume)
                      found = True
                      break
                  except exception.VolumeUnattached:
                      # The volume is not attached.  Treat it as NotFound
                      # by falling through.
                      pass
                  except exception.InvalidVolume as e:
                      raise exc.HTTPBadRequest(explanation=e.format_message())
          except exception.InstanceIsLocked as e:
              raise exc.HTTPConflict(explanation=e.format_message())
          except exception.InstanceInvalidState as state_error:
              common.raise_http_conflict_for_instance_invalid_state(state_error,
                      'swap_volume', server_id)
  
          if not found:
              msg = _("The volume was either invalid or not attached to the "
                      "instance.")
              raise exc.HTTPNotFound(explanation=msg)
  
      @wsgi.response(202)
      @extensions.expected_errors((400, 403, 404, 409))
      def delete(self, req, server_id, id):
          """Detach a volume from an instance."""
          context = req.environ['nova.context']
          context.can(va_policies.POLICY_ROOT % 'delete')
  
          volume_id = id
  
          instance = common.get_instance(self.compute_api, context, server_id)
          if instance.vm_state in (vm_states.SHELVED,
                                   vm_states.SHELVED_OFFLOADED):
              _check_request_version(req, '2.20', 'detach_volume',
                                     server_id, instance.vm_state)
          try:
              volume = self.volume_api.get(context, volume_id)
          except exception.VolumeNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
          bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(
                  context, instance.uuid)
          if not bdms:
              msg = _("Instance %s is not attached.") % server_id
              raise exc.HTTPNotFound(explanation=msg)
  
          found = False
          try:
              for bdm in bdms:
                  if bdm.volume_id != volume_id:
                      continue
                  if bdm.is_root:
                      msg = _("Can't detach root device volume")
                      raise exc.HTTPForbidden(explanation=msg)
                  try:
                      self.compute_api.detach_volume(context, instance, volume)
                      found = True
                      break
                  except exception.VolumeUnattached:
                      # The volume is not attached.  Treat it as NotFound
                      # by falling through.
                      pass
                  except exception.InvalidVolume as e:
                      raise exc.HTTPBadRequest(explanation=e.format_message())
                  except exception.InstanceUnknownCell as e:
                      raise exc.HTTPNotFound(explanation=e.format_message())
                  except exception.InvalidInput as e:
                      raise exc.HTTPBadRequest(explanation=e.format_message())
  
          except exception.InstanceIsLocked as e:
              raise exc.HTTPConflict(explanation=e.format_message())
          except exception.InstanceInvalidState as state_error:
              common.raise_http_conflict_for_instance_invalid_state(state_error,
                      'detach_volume', server_id)
  
          if not found:
              msg = _("volume_id not found: %s") % volume_id
              raise exc.HTTPNotFound(explanation=msg)
  
  
  def _translate_snapshot_detail_view(context, vol):
      """Maps keys for snapshots details view."""
  
      d = _translate_snapshot_summary_view(context, vol)
  
      # NOTE(gagupta): No additional data / lookups at the moment
      return d
  
  
  def _translate_snapshot_summary_view(context, vol):
      """Maps keys for snapshots summary view."""
      d = {}
  
      d['id'] = vol['id']
      d['volumeId'] = vol['volume_id']
      d['status'] = vol['status']
      # NOTE(gagupta): We map volume_size as the snapshot size
      d['size'] = vol['volume_size']
      d['createdAt'] = vol['created_at']
      d['displayName'] = vol['display_name']
      d['displayDescription'] = vol['display_description']
      return d
  
  
  class SnapshotController(wsgi.Controller):
      """The Snapshots API controller for the OpenStack API."""
  
      def __init__(self):
          self.volume_api = cinder.API()
          super(SnapshotController, self).__init__()
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(404)
      def show(self, req, id):
          """Return data about the given snapshot."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          try:
              vol = self.volume_api.get_snapshot(context, id)
          except exception.SnapshotNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
          return {'snapshot': _translate_snapshot_detail_view(context, vol)}
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @wsgi.response(202)
      @extensions.expected_errors(404)
      def delete(self, req, id):
          """Delete a snapshot."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          try:
              self.volume_api.delete_snapshot(context, id)
          except exception.SnapshotNotFound as e:
              raise exc.HTTPNotFound(explanation=e.format_message())
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(())
      def index(self, req):
          """Returns a summary list of snapshots."""
          return self._items(req, entity_maker=_translate_snapshot_summary_view)
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors(())
      def detail(self, req):
          """Returns a detailed list of snapshots."""
          return self._items(req, entity_maker=_translate_snapshot_detail_view)
  
      def _items(self, req, entity_maker):
          """Returns a list of snapshots, transformed through entity_maker."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          snapshots = self.volume_api.get_all_snapshots(context)
          limited_list = common.limited(snapshots, req)
          res = [entity_maker(context, snapshot) for snapshot in limited_list]
          return {'snapshots': res}
  
      @wsgi.Controller.api_version("2.1", MAX_PROXY_API_SUPPORT_VERSION)
      @extensions.expected_errors((400, 403))
      @validation.schema(volumes_schema.snapshot_create)
      def create(self, req, body):
          """Creates a new snapshot."""
          context = req.environ['nova.context']
          context.can(vol_policies.BASE_POLICY_NAME)
  
          snapshot = body['snapshot']
          volume_id = snapshot['volume_id']
  
          force = snapshot.get('force', False)
          force = strutils.bool_from_string(force, strict=True)
          if force:
              create_func = self.volume_api.create_snapshot_force
          else:
              create_func = self.volume_api.create_snapshot
  
          try:
              new_snapshot = create_func(context, volume_id,
                                         snapshot.get('display_name'),
                                         snapshot.get('display_description'))
          except exception.OverQuota as e:
              raise exc.HTTPForbidden(explanation=e.format_message())
  
          retval = _translate_snapshot_detail_view(context, new_snapshot)
          return {'snapshot': retval}
  
  
  class Volumes(extensions.V21APIExtensionBase):
      """Volumes support."""
  
      name = "Volumes"
      alias = ALIAS
      version = 1
  
      def get_resources(self):
          resources = []
  
          res = extensions.ResourceExtension(
              ALIAS, VolumeController(), collection_actions={'detail': 'GET'})
          resources.append(res)
  
          res = extensions.ResourceExtension('os-volumes_boot',
                                             inherits='servers')
          resources.append(res)
  
          res = extensions.ResourceExtension('os-volume_attachments',
                                             VolumeAttachmentController(),
                                             parent=dict(
                                                  member_name='server',
                                                  collection_name='servers'))
          resources.append(res)
  
          res = extensions.ResourceExtension(
              'os-snapshots', SnapshotController(),
              collection_actions={'detail': 'GET'})
          resources.append(res)
  
          return resources
  
      def get_controller_extensions(self):
          return []
  