#    Copyright (c) 2014 - 2019 StorPool
#    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.

"""StorPool block device driver"""

from __future__ import absolute_import

import platform

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
from oslo_utils import units
import six

from cinder import exception
from cinder.i18n import _
from cinder import interface
from cinder.volume import driver
from cinder.volume import volume_types

LOG = logging.getLogger(__name__)

storpool = importutils.try_import('storpool')
if storpool:
    from storpool import spapi
    from storpool import spconfig
    from storpool import spopenstack
    from storpool import sptypes


storpool_opts = [
    cfg.StrOpt('storpool_template',
               default=None,
               help='The StorPool template for volumes with no type.'),
    cfg.IntOpt('storpool_replication',
               default=3,
               help='The default StorPool chain replication value.  '
                    'Used when creating a volume with no specified type if '
                    'storpool_template is not set.  Also used for calculating '
                    'the apparent free space reported in the stats.'),
]

CONF = cfg.CONF
CONF.register_opts(storpool_opts)


class StorPoolConfigurationInvalid(exception.CinderException):
    message = _("Invalid parameter %(param)s in the %(section)s section "
                "of the /etc/storpool.conf file: %(error)s")


@interface.volumedriver
class StorPoolDriver(driver.VolumeDriver):
    """The StorPool block device driver.

    Version history:

    .. code-block:: none

        0.1.0   - Initial driver
        0.2.0   - Bring the driver up to date with Kilo and Liberty:
                  - implement volume retyping and migrations
                  - use the driver.*VD ABC metaclasses
                  - bugfix: fall back to the configured StorPool template
        1.0.0   - Imported into OpenStack Liberty with minor fixes
        1.1.0   - Bring the driver up to date with Liberty and Mitaka:
                  - drop the CloneableVD and RetypeVD base classes
                  - enable faster volume copying by specifying
                    sparse_volume_copy=true in the stats report
        1.1.1   - Fix the internal _storpool_client_id() method to
                  not break on an unknown host name or UUID; thus,
                  remove the StorPoolConfigurationMissing exception.
        1.1.2   - Bring the driver up to date with Pike: do not
                  translate the error messages
        1.2.0   - Inherit from VolumeDriver, implement get_pool()
        1.2.1   - Implement interface.volumedriver, add CI_WIKI_NAME,
                  fix the docstring formatting
        1.2.2   - Reintroduce the driver into OpenStack Queens,
                  add ignore_errors to the internal _detach_volume() method
        1.2.3   - Advertise some more driver capabilities.
    """

    VERSION = '1.2.3'
    CI_WIKI_NAME = 'StorPool_CI'

    def __init__(self, *args, **kwargs):
        super(StorPoolDriver, self).__init__(*args, **kwargs)
        self.configuration.append_config_values(storpool_opts)
        self._sp_config = None
        self._ourId = None
        self._ourIdInt = None
        self._attach = None

    @staticmethod
    def get_driver_options():
        return storpool_opts

    def _backendException(self, e):
        return exception.VolumeBackendAPIException(data=six.text_type(e))

    def _template_from_volume(self, volume):
        default = self.configuration.storpool_template
        vtype = volume['volume_type']
        if vtype is not None:
            specs = volume_types.get_volume_type_extra_specs(vtype['id'])
            if specs is not None:
                return specs.get('storpool_template', default)
        return default

    def get_pool(self, volume):
        template = self._template_from_volume(volume)
        if template is None:
            return 'default'
        else:
            return 'template_' + template

    def create_volume(self, volume):
        size = int(volume['size']) * units.Gi
        name = self._attach.volumeName(volume['id'])
        template = self._template_from_volume(volume)
        try:
            if template is None:
                self._attach.api().volumeCreate({
                    'name': name,
                    'size': size,
                    'replication': self.configuration.storpool_replication
                })
            else:
                self._attach.api().volumeCreate({
                    'name': name,
                    'size': size,
                    'template': template
                })
        except spapi.ApiError as e:
            raise self._backendException(e)

    def _storpool_client_id(self, connector):
        hostname = connector['host']
        if hostname == self.host or hostname == CONF.host:
            hostname = platform.node()
        try:
            cfg = spconfig.SPConfig(section=hostname)
            return int(cfg['SP_OURID'])
        except KeyError:
            return 65
        except Exception as e:
            raise StorPoolConfigurationInvalid(
                section=hostname, param='SP_OURID', error=e)

    def validate_connector(self, connector):
        return self._storpool_client_id(connector) >= 0

    def initialize_connection(self, volume, connector):
        return {'driver_volume_type': 'storpool',
                'data': {
                    'client_id': self._storpool_client_id(connector),
                    'volume': volume['id'],
                }}

    def terminate_connection(self, volume, connector, **kwargs):
        pass

    def create_snapshot(self, snapshot):
        volname = self._attach.volumeName(snapshot['volume_id'])
        name = self._attach.snapshotName('snap', snapshot['id'])
        try:
            self._attach.api().snapshotCreate(volname, {'name': name})
        except spapi.ApiError as e:
            raise self._backendException(e)

    def create_volume_from_snapshot(self, volume, snapshot):
        size = int(volume['size']) * units.Gi
        volname = self._attach.volumeName(volume['id'])
        name = self._attach.snapshotName('snap', snapshot['id'])
        try:
            self._attach.api().volumeCreate({
                'name': volname,
                'size': size,
                'parent': name
            })
        except spapi.ApiError as e:
            raise self._backendException(e)

    def create_cloned_volume(self, volume, src_vref):
        refname = self._attach.volumeName(src_vref['id'])
        snapname = self._attach.snapshotName('clone', volume['id'])
        try:
            self._attach.api().snapshotCreate(refname, {'name': snapname})
        except spapi.ApiError as e:
            raise self._backendException(e)

        size = int(volume['size']) * units.Gi
        volname = self._attach.volumeName(volume['id'])
        try:
            self._attach.api().volumeCreate({
                'name': volname,
                'size': size,
                'parent': snapname
            })
        except spapi.ApiError as e:
            raise self._backendException(e)
        finally:
            try:
                self._attach.api().snapshotDelete(snapname)
            except spapi.ApiError as e:
                # ARGH!
                LOG.error("Could not delete the temp snapshot %(name)s: "
                          "%(msg)s",
                          {'name': snapname, 'msg': e})

    def create_export(self, context, volume, connector):
        pass

    def remove_export(self, context, volume):
        pass

    def delete_volume(self, volume):
        name = self._attach.volumeName(volume['id'])
        try:
            self._attach.api().volumesReassign(
                json=[{"volume": name, "detach": "all"}])
            self._attach.api().volumeDelete(name)
        except spapi.ApiError as e:
            if e.name == 'objectDoesNotExist':
                pass
            else:
                raise self._backendException(e)

    def delete_snapshot(self, snapshot):
        name = self._attach.snapshotName('snap', snapshot['id'])
        try:
            self._attach.api().volumesReassign(
                json=[{"snapshot": name, "detach": "all"}])
            self._attach.api().snapshotDelete(name)
        except spapi.ApiError as e:
            if e.name == 'objectDoesNotExist':
                pass
            else:
                raise self._backendException(e)

    def check_for_setup_error(self):
        if storpool is None:
            msg = _('storpool libraries not found')
            raise exception.VolumeBackendAPIException(data=msg)

        self._attach = spopenstack.AttachDB(log=LOG)
        try:
            self._attach.api()
        except Exception as e:
            LOG.error("StorPoolDriver API initialization failed: %s", e)
            raise

    def get_volume_stats(self, refresh=False):
        if refresh:
            self._update_volume_stats()

        return self._stats

    def _update_volume_stats(self):
        try:
            dl = self._attach.api().disksList()
            templates = self._attach.api().volumeTemplatesList()
        except spapi.ApiError as e:
            raise self._backendException(e)
        total = 0
        used = 0
        free = 0
        agSize = 512 * units.Mi
        for (id, desc) in dl.items():
            if desc.generationLeft != -1:
                continue
            total += desc.agCount * agSize
            used += desc.agAllocated * agSize
            free += desc.agFree * agSize * 4096 / (4096 + 128)

        # Report the free space as if all new volumes will be created
        # with StorPool replication 3; anything else is rare.
        free /= self.configuration.storpool_replication

        space = {
            'total_capacity_gb': total / units.Gi,
            'free_capacity_gb': free / units.Gi,
            'reserved_percentage': 0,
            'multiattach': True,
            'QoS_support': False,
            'thick_provisioning_support': False,
            'thin_provisioning_support': True,
        }

        pools = [dict(space, pool_name='default')]

        pools += [dict(space,
                       pool_name='template_' + t.name,
                       storpool_template=t.name
                       ) for t in templates]

        self._stats = {
            'volume_backend_name': self.configuration.safe_get(
                'volume_backend_name') or 'storpool',
            'vendor_name': 'StorPool',
            'driver_version': self.VERSION,
            'storage_protocol': 'storpool',

            'sparse_copy_volume': True,

            'pools': pools
        }

    def _attach_volume(self, context, volume, properties, remote=False):
        if remote:
            return super(StorPoolDriver, self)._attach_volume(
                context, volume, properties, remote=remote)
        req_id = context.request_id
        req = self._attach.get().get(req_id, None)
        if req is None:
            req = {
                'volume': self._attach.volumeName(volume['id']),
                'type': 'cinder-attach',
                'id': context.request_id,
                'rights': 2,
                'volsnap': False,
                'remove_on_detach': True
            }
            self._attach.add(req_id, req)
        name = req['volume']
        self._attach.sync(req_id, None)
        return {'device': {'path': '/dev/storpool/' + name,
                'storpool_attach_req': req_id}}, volume

    def _detach_volume(self, context, attach_info, volume, properties,
                       force=False, remote=False, ignore_errors=False):
        if remote:
            return super(StorPoolDriver, self)._detach_volume(
                context, attach_info, volume, properties,
                force=force, remote=remote, ignore_errors=ignore_errors)
        try:
            req_id = attach_info.get('device', {}).get(
                'storpool_attach_req', context.request_id)
            req = self._attach.get()[req_id]
            name = req['volume']
            self._attach.sync(req_id, name)
            if req.get('remove_on_detach', False):
                self._attach.remove(req_id)
        except BaseException:
            if not ignore_errors:
                raise

    def backup_volume(self, context, backup, backup_service):
        volume = self.db.volume_get(context, backup['volume_id'])
        req_id = context.request_id
        volname = self._attach.volumeName(volume['id'])
        name = self._attach.volsnapName(volume['id'], req_id)
        try:
            self._attach.api().snapshotCreate(volname, {'name': name})
        except spapi.ApiError as e:
            raise self._backendException(e)
        self._attach.add(req_id, {
            'volume': name,
            'type': 'backup',
            'id': req_id,
            'rights': 1,
            'volsnap': True
        })
        try:
            return super(StorPoolDriver, self).backup_volume(
                context, backup, backup_service)
        finally:
            self._attach.remove(req_id)
            try:
                self._attach.api().snapshotDelete(name)
            except spapi.ApiError as e:
                LOG.error(
                    'Could not remove the temp snapshot %(name)s for '
                    '%(vol)s: %(err)s',
                    {'name': name, 'vol': volname, 'err': e})

    def copy_volume_to_image(self, context, volume, image_service, image_meta):
        req_id = context.request_id
        volname = self._attach.volumeName(volume['id'])
        name = self._attach.volsnapName(volume['id'], req_id)
        try:
            self._attach.api().snapshotCreate(volname, {'name': name})
        except spapi.ApiError as e:
            raise self._backendException(e)
        self._attach.add(req_id, {
            'volume': name,
            'type': 'copy-from',
            'id': req_id,
            'rights': 1,
            'volsnap': True
        })
        try:
            return super(StorPoolDriver, self).copy_volume_to_image(
                context, volume, image_service, image_meta)
        finally:
            self._attach.remove(req_id)
            try:
                self._attach.api().snapshotDelete(name)
            except spapi.ApiError as e:
                LOG.error(
                    'Could not remove the temp snapshot %(name)s for '
                    '%(vol)s: %(err)s',
                    {'name': name, 'vol': volname, 'err': e})

    def copy_image_to_volume(self, context, volume, image_service, image_id):
        req_id = context.request_id
        name = self._attach.volumeName(volume['id'])
        self._attach.add(req_id, {
            'volume': name,
            'type': 'copy-to',
            'id': req_id,
            'rights': 2
        })
        try:
            return super(StorPoolDriver, self).copy_image_to_volume(
                context, volume, image_service, image_id)
        finally:
            self._attach.remove(req_id)

    def extend_volume(self, volume, new_size):
        size = int(new_size) * units.Gi
        name = self._attach.volumeName(volume['id'])
        try:
            upd = sptypes.VolumeUpdateDesc(size=size)
            self._attach.api().volumeUpdate(name, upd)
        except spapi.ApiError as e:
            raise self._backendException(e)

    def ensure_export(self, context, volume):
        # Already handled by Nova's AttachDB, we hope.
        # Maybe it should move here, but oh well.
        pass

    def retype(self, context, volume, new_type, diff, host):
        update = {}

        if diff['encryption']:
            LOG.error('Retype of encryption type not supported.')
            return False

        templ = self.configuration.storpool_template
        repl = self.configuration.storpool_replication
        if diff['extra_specs']:
            for (k, v) in diff['extra_specs'].items():
                if k == 'volume_backend_name':
                    if v[0] != v[1]:
                        # Retype of a volume backend not supported yet,
                        # the volume needs to be migrated.
                        return False
                elif k == 'storpool_template':
                    if v[0] != v[1]:
                        if v[1] is not None:
                            update['template'] = v[1]
                        elif templ is not None:
                            update['template'] = templ
                        else:
                            update['replication'] = repl
                elif v[0] != v[1]:
                    LOG.error('Retype of extra_specs "%s" not '
                              'supported yet.', k)
                    return False

        if update:
            name = self._attach.volumeName(volume['id'])
            try:
                upd = sptypes.VolumeUpdateDesc(**update)
                self._attach.api().volumeUpdate(name, upd)
            except spapi.ApiError as e:
                raise self._backendException(e)

        return True

    def update_migrated_volume(self, context, volume, new_volume,
                               original_volume_status):
        orig_id = volume['id']
        orig_name = self._attach.volumeName(orig_id)
        temp_id = new_volume['id']
        temp_name = self._attach.volumeName(temp_id)
        vols = {v.name: True for v in self._attach.api().volumesList()}
        if temp_name not in vols:
            LOG.error('StorPool update_migrated_volume(): it seems '
                      'that the StorPool volume "%(tid)s" was not '
                      'created as part of the migration from '
                      '"%(oid)s".', {'tid': temp_id, 'oid': orig_id})
            return {'_name_id': new_volume['_name_id'] or new_volume['id']}
        elif orig_name in vols:
            LOG.error('StorPool update_migrated_volume(): both '
                      'the original volume "%(oid)s" and the migrated '
                      'StorPool volume "%(tid)s" seem to exist on '
                      'the StorPool cluster.',
                      {'oid': orig_id, 'tid': temp_id})
            return {'_name_id': new_volume['_name_id'] or new_volume['id']}
        else:
            try:
                self._attach.api().volumeUpdate(temp_name,
                                                {'rename': orig_name})
                return {'_name_id': None}
            except spapi.ApiError as e:
                LOG.error('StorPool update_migrated_volume(): '
                          'could not rename %(tname)s to %(oname)s: '
                          '%(err)s',
                          {'tname': temp_name, 'oname': orig_name, 'err': e})
                return {'_name_id': new_volume['_name_id'] or new_volume['id']}
