# Copyright 2012 OpenStack Foundation.
# 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.
import glance_store
from io import BytesIO
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import excutils
import tarfile
import webob.exc

import glance.api.policy
from glance.common import exception
from glance.common import utils
from glance.common import wsgi
import glance.db
import glance.gateway
from glance import i18n
import glance.notifier

try:
    import xml.etree.cElementTree as ET
except ImportError:
    import xml.etree.ElementTree as ET

LOG = logging.getLogger(__name__)
_ = i18n._
_LE = i18n._LE

CONF = cfg.CONF
CONF.import_opt('disk_formats', 'glance.common.config', group='image_format')
CONF.import_opt('ovf_metadata_properties', 'glance.common.config')


class ImageDataController(object):

    class OVAImageExtractor(object):
        """
        A class that extracts the disk image and ovf file from an OVA
        tarball. Parses the OVF file for metadata of interest.
        Allows upload of disk image to Glance with included metadata.
        """
        def __init__(self):
            self.interested_properties = CONF.ovf_metadata_properties
            if not self.interested_properties:
                LOG.warn(_('Interested OVF metadata was not specified'))

        def extract(self, ova):
            """
            Extracts disk image and ovf from ova file and calls ovf parser

            :param ova: a file object containing the ova file
            :returns: a tuple of extracted disk file object and dictionary of
                      properties parsed from the ovf file
            """
            with tarfile.open(fileobj=ova) as tar_file:
                disk_name, properties = None, None
                filenames = tar_file.getnames()

                ovf_filename = next((filename for filename in filenames if '.ovf' in
                                    filename), None)
                if ovf_filename:
                    ovf = tar_file.extractfile(ovf_filename)
                    properties = self._parse_OVF(ovf)
                    ovf.close()

                for filename in filenames:
                    if any(disk_format in filename for disk_format in
                           CONF.image_format.disk_formats):
                        disk_name = filename

                disk = tar_file.extractfile(disk_name)
                if not disk:
                    msg = _('Could not find valid disk image in OVA package')
                    LOG.error(msg)
                return (disk, properties)

        def _parse_OVF(self, ovf):
            """
            Parses the OVF file

            Qualified namespaces are removed from the included properties.

            :param ovf: a file object containing the ovf file
            :returns: a dictionary of properties
            """
            properties = {}
            for event, elem in ET.iterparse(ovf):
                if event == 'end':
                    tag = elem.tag.split('}', 1)[1] if '}' in elem.tag else elem.tag
                    if tag in self.interested_properties:
                        properties[tag] = elem.text.strip() if elem.text else ''
                    elem.clear()
            return properties


    def __init__(self, db_api=None, store_api=None,
                 policy_enforcer=None, notifier=None,
                 gateway=None):
        if gateway is None:
            db_api = db_api or glance.db.get_api()
            store_api = store_api or glance_store
            policy = policy_enforcer or glance.api.policy.Enforcer()
            notifier = notifier or glance.notifier.Notifier()
            gateway = glance.gateway.Gateway(db_api, store_api,
                                             notifier, policy)
        self.gateway = gateway

    def _restore(self, image_repo, image):
        """
        Restore the image to queued status.

        :param image_repo: The instance of ImageRepo
        :param image: The image will be restored
        """
        try:
            if image_repo and image:
                image.status = 'queued'
                image_repo.save(image)
        except Exception as e:
            msg = (_LE("Unable to restore image %(image_id)s: %(e)s") %
                   {'image_id': image.image_id,
                    'e': utils.exception_to_str(e)})
            LOG.exception(msg)

    @utils.mutating
    def upload(self, req, image_id, data, size):
        image_repo = self.gateway.get_repo(req.context)
        image = None
        try:
            image = image_repo.get(image_id)
            image.status = 'saving'

            if image.container_format == u'ova':
                ova = data.read()
                extractor = self.OVAImageExtractor()
                disk, properties = extractor.extract(BytesIO(ova))
                image.extra_properties.update(properties)
                if disk:
                    data = disk # Set new data
                    disk.seek(0, 2)
                    size = disk.tell() # Set new data size
                    disk.seek(0)

            try:
                image_repo.save(image)
                image.set_data(data, size)
                image_repo.save(image, from_state='saving')
            except (exception.ImageNotFound, exception.Conflict):
                msg = (_("Image %s could not be found after upload. "
                         "The image may have been deleted during the "
                         "upload, cleaning up the chunks uploaded.") %
                       image_id)
                LOG.warn(msg)
                # NOTE(sridevi): Cleaning up the uploaded chunks.
                try:
                    image.delete()
                except exception.ImageNotFound:
                    # NOTE(sridevi): Ignore this exception
                    pass
                raise webob.exc.HTTPGone(explanation=msg,
                                         request=req,
                                         content_type='text/plain')

        except ValueError as e:
            LOG.debug("Cannot save data for image %(id)s: %(e)s",
                      {'id': image_id, 'e': utils.exception_to_str(e)})
            self._restore(image_repo, image)
            raise webob.exc.HTTPBadRequest(
                explanation=utils.exception_to_str(e))

        except glance_store.StoreAddDisabled:
            msg = _("Error in store configuration. Adding images to store "
                    "is disabled.")
            LOG.exception(msg)
            self._restore(image_repo, image)
            raise webob.exc.HTTPGone(explanation=msg, request=req,
                                     content_type='text/plain')

        except exception.InvalidImageStatusTransition as e:
            msg = utils.exception_to_str(e)
            LOG.exception(msg)
            raise webob.exc.HTTPConflict(explanation=e.msg, request=req)

        except exception.Forbidden as e:
            msg = ("Not allowed to upload image data for image %s" %
                   image_id)
            LOG.debug(msg)
            raise webob.exc.HTTPForbidden(explanation=msg, request=req)

        except exception.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)

        except glance_store.StorageFull as e:
            msg = _("Image storage media "
                    "is full: %s") % utils.exception_to_str(e)
            LOG.error(msg)
            self._restore(image_repo, image)
            raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
                                                      request=req)

        except exception.StorageQuotaFull as e:
            msg = _("Image exceeds the storage "
                    "quota: %s") % utils.exception_to_str(e)
            LOG.error(msg)
            self._restore(image_repo, image)
            raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
                                                      request=req)

        except exception.ImageSizeLimitExceeded as e:
            msg = _("The incoming image is "
                    "too large: %s") % utils.exception_to_str(e)
            LOG.error(msg)
            self._restore(image_repo, image)
            raise webob.exc.HTTPRequestEntityTooLarge(explanation=msg,
                                                      request=req)

        except glance_store.StorageWriteDenied as e:
            msg = _("Insufficient permissions on image "
                    "storage media: %s") % utils.exception_to_str(e)
            LOG.error(msg)
            self._restore(image_repo, image)
            raise webob.exc.HTTPServiceUnavailable(explanation=msg,
                                                   request=req)

        except webob.exc.HTTPGone as e:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Failed to upload image data due to HTTP error"))

        except webob.exc.HTTPError as e:
            with excutils.save_and_reraise_exception():
                LOG.error(_LE("Failed to upload image data due to HTTP error"))
                self._restore(image_repo, image)

        except Exception as e:
            with excutils.save_and_reraise_exception():
                LOG.exception(_LE("Failed to upload image data due to "
                                  "internal error"))
                self._restore(image_repo, image)

    def download(self, req, image_id):
        image_repo = self.gateway.get_repo(req.context)
        try:
            image = image_repo.get(image_id)
            if image.status == 'deactivated':
                msg = _('The requested image has been deactivated. '
                        'Image data download is forbidden.')
                raise exception.Forbidden(message=msg)
            if not image.locations:
                raise exception.ImageDataNotFound()
        except exception.ImageDataNotFound as e:
            raise webob.exc.HTTPNoContent(explanation=e.msg)
        except exception.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to download image '%s'", image_id)
            raise webob.exc.HTTPForbidden(explanation=e.msg)

        return image


class RequestDeserializer(wsgi.JSONRequestDeserializer):

    def upload(self, request):
        try:
            request.get_content_type(('application/octet-stream',))
        except exception.InvalidContentType as e:
            raise webob.exc.HTTPUnsupportedMediaType(explanation=e.msg)

        image_size = request.content_length or None
        return {'size': image_size, 'data': request.body_file}


class ResponseSerializer(wsgi.JSONResponseSerializer):

    def download(self, response, image):
        offset, chunk_size = 0, None
        range_val = response.request.get_content_range()

        if range_val:
            # NOTE(flaper87): if not present, both, start
            # and stop, will be None.
            if range_val.start is not None:
                offset = range_val.start

            if range_val.stop is not None:
                chunk_size = range_val.stop - offset

        response.headers['Content-Type'] = 'application/octet-stream'

        try:
            # NOTE(markwash): filesystem store (and maybe others?) cause a
            # problem with the caching middleware if they are not wrapped in
            # an iterator very strange
            response.app_iter = iter(image.get_data(offset=offset,
                                                    chunk_size=chunk_size))
        except glance_store.NotFound as e:
            raise webob.exc.HTTPNotFound(explanation=e.msg)
        except glance_store.RemoteServiceUnavailable as e:
            raise webob.exc.HTTPServiceUnavailable(explanation=e.msg)
        except (glance_store.StoreGetNotSupported,
                glance_store.StoreRandomGetNotSupported) as e:
            raise webob.exc.HTTPBadRequest(explanation=e.msg)
        except exception.Forbidden as e:
            LOG.debug("User not permitted to download image '%s'", image)
            raise webob.exc.HTTPForbidden(explanation=e.msg)
        # NOTE(saschpe): "response.app_iter = ..." currently resets Content-MD5
        # (https://github.com/Pylons/webob/issues/86), so it should be set
        # afterwards for the time being.
        if image.checksum:
            response.headers['Content-MD5'] = image.checksum
        # NOTE(markwash): "response.app_iter = ..." also erroneously resets the
        # content-length
        response.headers['Content-Length'] = str(image.size)

    def upload(self, response, result):
        response.status_int = 204


def create_resource():
    """Image data resource factory method"""
    deserializer = RequestDeserializer()
    serializer = ResponseSerializer()
    controller = ImageDataController()
    return wsgi.Resource(controller, deserializer, serializer)
