#    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 attachments API."""
from http import HTTPStatus

from oslo_log import log as logging
import webob

from cinder.api import api_utils
from cinder.api import common
from cinder.api import microversions as mv
from cinder.api.openstack import wsgi
from cinder.api.schemas import attachments as attachment
from cinder.api.v3.views import attachments as attachment_views
from cinder.api import validation
from cinder import context as cinder_context
from cinder import exception
from cinder.i18n import _
from cinder import objects
from cinder.objects import fields
from cinder.policies import attachments as attachment_policy
from cinder.volume import api as volume_api
from cinder.volume import volume_utils


LOG = logging.getLogger(__name__)


class AttachmentsController(wsgi.Controller):
    """The Attachments API controller for the OpenStack API."""

    _view_builder_class = attachment_views.ViewBuilder

    allowed_filters = {'volume_id', 'status', 'instance_id', 'attach_status'}

    def __init__(self, ext_mgr=None):
        """Initialize controller class."""
        self.volume_api = volume_api.API()
        self.ext_mgr = ext_mgr
        super(AttachmentsController, self).__init__()

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    def show(self, req, id):
        """Return data about the given attachment."""
        context = req.environ['cinder.context']
        attachment = objects.VolumeAttachment.get_by_id(context, id)
        volume = objects.Volume.get_by_id(cinder_context.get_admin_context(),
                                          attachment.volume_id)
        if volume.admin_metadata and 'format' in volume.admin_metadata:
            attachment.connection_info['format'] = (
                volume.admin_metadata['format'])
        return attachment_views.ViewBuilder.detail(attachment)

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    def index(self, req):
        """Return a summary list of attachments."""
        attachments = self._items(req)
        return attachment_views.ViewBuilder.list(attachments)

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    def detail(self, req):
        """Return a detailed list of attachments."""
        attachments = self._items(req)
        return attachment_views.ViewBuilder.list(attachments, detail=True)

    @common.process_general_filtering('attachment')
    def _process_attachment_filtering(self, context=None, filters=None,
                                      req_version=None):
        api_utils.remove_invalid_filter_options(context, filters,
                                                self.allowed_filters)

    def _items(self, req):
        """Return a list of attachments, transformed through view builder."""
        context = req.environ['cinder.context']
        req_version = req.api_version_request

        # Pop out non search_opts and create local variables
        search_opts = req.GET.copy()
        sort_keys, sort_dirs = common.get_sort_params(search_opts)
        marker, limit, offset = common.get_pagination_params(search_opts)

        self._process_attachment_filtering(context=context,
                                           filters=search_opts,
                                           req_version=req_version)
        if search_opts.get('instance_id', None):
            search_opts['instance_uuid'] = search_opts.pop('instance_id', None)
        if context.is_admin and 'all_tenants' in search_opts:
            del search_opts['all_tenants']
            return objects.VolumeAttachmentList.get_all(
                context, search_opts=search_opts, marker=marker, limit=limit,
                offset=offset, sort_keys=sort_keys, sort_direction=sort_dirs)
        else:
            return objects.VolumeAttachmentList.get_all_by_project(
                context, context.project_id, search_opts=search_opts,
                marker=marker, limit=limit, offset=offset, sort_keys=sort_keys,
                sort_direction=sort_dirs)

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    @wsgi.response(HTTPStatus.OK)
    @validation.schema(attachment.create, mv.NEW_ATTACH,
                       mv.get_prior_version(mv.ATTACHMENT_CREATE_MODE_ARG))
    @validation.schema(attachment.create_v354, mv.ATTACHMENT_CREATE_MODE_ARG)
    def create(self, req, body):
        """Create an attachment.

        This method can be used to create an empty attachment (reserve) or to
        create and initialize a volume attachment based on the provided input
        parameters.

        If the caller does not yet have the connector information but needs to
        reserve an attachment for the volume (ie Nova BootFromVolume) the
        create can be called with just the volume-uuid and the server
        identifier. This will reserve an attachment, mark the volume as
        reserved and prevent any new attachment_create calls from being made
        until the attachment is updated (completed).

        The alternative is that the connection can be reserved and initialized
        all at once with a single call if the caller has all of the required
        information (connector data) at the time of the call.

        NOTE: In Nova terms server == instance, the server_id parameter
        referenced below is the UUID of the Instance, for non-nova consumers
        this can be a server UUID or some other arbitrary unique identifier.

        Starting from microversion 3.54, we can pass the attach mode as
        argument in the request body.

        Expected format of the input parameter 'body':

        .. code-block:: json

            {
                "attachment":
                {
                    "volume_uuid": "volume-uuid",
                    "instance_uuid": "null|nova-server-uuid",
                    "connector": "null|<connector-object>",
                    "mode": "null|rw|ro"
                }
            }

        Example connector:

        .. code-block:: json

            {
                "connector":
                {
                    "initiator": "iqn.1993-08.org.debian:01:cad181614cec",
                    "ip": "192.168.1.20",
                    "platform": "x86_64",
                    "host": "tempest-1",
                    "os_type": "linux2",
                    "multipath": false,
                    "mountpoint": "/dev/vdb",
                    "mode": "null|rw|ro"
                }
            }

        NOTE all that's required for a reserve is volume_uuid
        and an instance_uuid.

        returns: A summary view of the attachment object
        """
        context = req.environ['cinder.context']
        instance_uuid = body['attachment'].get('instance_uuid')
        volume_uuid = body['attachment']['volume_uuid']
        volume_ref = objects.Volume.get_by_id(
            context,
            volume_uuid)
        args = {'connector': body['attachment'].get('connector', None)}

        if req.api_version_request.matches(mv.ATTACHMENT_CREATE_MODE_ARG):
            # We check for attach_mode here and default to `null`
            # if nothing's provided.  This seems odd to not just
            # set `rw`, BUT we want to keep compatability with
            # setting the mode via the connector for now, so we
            # use `null` as an identifier to distinguish that case
            args['attach_mode'] = body['attachment'].get('mode', 'null')

        err_msg = None
        try:
            attachment_ref = (
                self.volume_api.attachment_create(context,
                                                  volume_ref,
                                                  instance_uuid,
                                                  **args))
        except (exception.NotAuthorized,
                exception.InvalidVolume):
            raise
        except exception.CinderException as ex:
            err_msg = _(
                "Unable to create attachment for volume (%s).") % ex.msg
            LOG.exception(err_msg)
        except Exception:
            err_msg = _("Unable to create attachment for volume.")
            LOG.exception(err_msg)
        finally:
            if err_msg:
                raise webob.exc.HTTPInternalServerError(explanation=err_msg)
        return attachment_views.ViewBuilder.detail(attachment_ref)

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    @validation.schema(attachment.update)
    def update(self, req, id, body):
        """Update an attachment record.

        Update a reserved attachment record with connector information and set
        up the appropriate connection_info from the driver.

        Expected format of the input parameter 'body':

        .. code:: json

            {
                "attachment":
                {
                    "connector":
                    {
                        "initiator": "iqn.1993-08.org.debian:01:cad181614cec",
                        "ip": "192.168.1.20",
                        "platform": "x86_64",
                        "host": "tempest-1",
                        "os_type": "linux2",
                        "multipath": false,
                        "mountpoint": "/dev/vdb",
                        "mode": "None|rw|ro"
                    }
                }
            }

        """
        context = req.environ['cinder.context']
        attachment_ref = (
            objects.VolumeAttachment.get_by_id(context, id))
        connector = body['attachment']['connector']
        err_msg = None
        try:
            attachment_ref = (
                self.volume_api.attachment_update(context,
                                                  attachment_ref,
                                                  connector))
        except (exception.NotAuthorized, exception.Invalid):
            raise
        except exception.CinderException as ex:
            err_msg = (
                _("Unable to update attachment (%s).") % ex.msg)
            LOG.exception(err_msg)
        except Exception:
            err_msg = _("Unable to update the attachment.")
            LOG.exception(err_msg)
        finally:
            if err_msg:
                raise webob.exc.HTTPInternalServerError(explanation=err_msg)

        # TODO(jdg): Test this out some more, do we want to return and object
        # or a dict?
        return attachment_views.ViewBuilder.detail(attachment_ref)

    @wsgi.Controller.api_version(mv.NEW_ATTACH)
    def delete(self, req, id):
        """Delete an attachment.

        Disconnects/Deletes the specified attachment, returns a list of any
        known shared attachment-id's for the effected backend device.

        returns: A summary list of any attachments sharing this connection

        """
        context = req.environ['cinder.context']
        attachment = objects.VolumeAttachment.get_by_id(context, id)
        attachments = self.volume_api.attachment_delete(context, attachment)
        return attachment_views.ViewBuilder.list(attachments)

    @wsgi.response(HTTPStatus.NO_CONTENT)
    @wsgi.Controller.api_version(mv.NEW_ATTACH_COMPLETION)
    @wsgi.action('os-complete')
    def complete(self, req, id, body):
        """Mark a volume attachment process as completed (in-use)."""
        context = req.environ['cinder.context']
        attachment_ref = (
            objects.VolumeAttachment.get_by_id(context, id))
        volume_ref = objects.Volume.get_by_id(
            context,
            attachment_ref.volume_id)
        context.authorize(attachment_policy.COMPLETE_POLICY,
                          target_obj=attachment_ref)
        attachment_ref.update(
            {'attach_status': fields.VolumeAttachStatus.ATTACHED})
        attachment_ref.save()
        volume_ref.update({'status': 'in-use', 'attach_status': 'attached'})
        volume_ref.save()
        volume_utils.notify_about_volume_usage(context, volume_ref,
                                               "attach.end")


def create_resource(ext_mgr):
    """Create the wsgi resource for this controller."""
    return wsgi.Resource(AttachmentsController(ext_mgr))
