# Copyright (C) 2016, Hitachi, Ltd.
#
#    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.
#
"""Common module for Hitachi VSP Driver."""

import abc
import re

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

from cinder import coordination
from cinder import exception
from cinder import utils as cinder_utils
from cinder.volume import configuration
from cinder.volume.drivers.hitachi import vsp_utils as utils
from cinder.volume import utils as volume_utils


VERSION = '1.0.0'

_COPY_METHOD = set(['FULL', 'THIN'])

_INHERITED_VOLUME_OPTS = [
    'volume_backend_name',
    'volume_driver',
    'reserved_percentage',
    'use_multipath_for_image_xfer',
    'enforce_multipath_for_image_xfer',
    'num_volume_device_scan_tries',
]

common_opts = [
    cfg.StrOpt(
        'vsp_storage_id',
        help='Product number of the storage system.'),
    cfg.StrOpt(
        'vsp_pool',
        help='Pool number or pool name of the DP pool.'),
    cfg.StrOpt(
        'vsp_thin_pool',
        help='Pool number or pool name of the Thin Image pool.'),
    cfg.StrOpt(
        'vsp_ldev_range',
        help='Range of the LDEV numbers in the format of \'xxxx-yyyy\' that '
             'can be used by the driver. Values can be in decimal format '
             '(e.g. 1000) or in colon-separated hexadecimal format '
             '(e.g. 00:03:E8).'),
    cfg.StrOpt(
        'vsp_default_copy_method',
        default='FULL',
        choices=['FULL', 'THIN'],
        help='Method of volume copy. FULL indicates full data copy by '
             'Shadow Image and THIN indicates differential data copy by Thin '
             'Image.'),
    cfg.IntOpt(
        'vsp_copy_speed',
        min=1,
        max=15,
        default=3,
        help='Speed at which data is copied by Shadow Image. 1 or 2 indicates '
             'low speed, 3 indicates middle speed, and a value between 4 and '
             '15 indicates high speed.'),
    cfg.IntOpt(
        'vsp_copy_check_interval',
        min=1,
        max=600,
        default=3,
        help='Interval in seconds at which volume pair synchronization status '
             'is checked when volume pairs are created.'),
    cfg.IntOpt(
        'vsp_async_copy_check_interval',
        min=1,
        max=600,
        default=10,
        help='Interval in seconds at which volume pair synchronization status '
             'is checked when volume pairs are deleted.'),
    cfg.ListOpt(
        'vsp_target_ports',
        help='IDs of the storage ports used to attach volumes to the '
             'controller node. To specify multiple ports, connect them by '
             'commas (e.g. CL1-A,CL2-A).'),
    cfg.ListOpt(
        'vsp_compute_target_ports',
        help='IDs of the storage ports used to attach volumes to compute '
             'nodes. To specify multiple ports, connect them by commas '
             '(e.g. CL1-A,CL2-A).'),
    cfg.BoolOpt(
        'vsp_group_request',
        default=False,
        help='If True, the driver will create host groups or iSCSI targets on '
             'storage ports as needed.'),
]

_REQUIRED_COMMON_OPTS = [
    'vsp_storage_id',
    'vsp_pool',
]

CONF = cfg.CONF
CONF.register_opts(common_opts, group=configuration.SHARED_CONF_GROUP)

LOG = logging.getLogger(__name__)
MSG = utils.VSPMsg


def _str2int(num):
    """Convert a string into an integer."""
    if not num:
        return None
    if num.isdigit():
        return int(num)
    if not re.match(r'\w\w:\w\w:\w\w', num):
        return None
    try:
        return int(num.replace(':', ''), 16)
    except ValueError:
        return None


@six.add_metaclass(abc.ABCMeta)
class VSPCommon(object):
    """Common class for Hitachi VSP Driver."""

    def __init__(self, conf, driverinfo, db):
        """Initialize instance variables."""
        self.conf = conf
        self.db = db
        self.ctxt = None
        self.lock = {}
        self.driver_info = driverinfo
        self.storage_info = {
            'protocol': driverinfo['proto'],
            'pool_id': None,
            'ldev_range': [],
            'controller_ports': [],
            'compute_ports': [],
            'pair_ports': [],
            'wwns': {},
            'portals': {},
            'output_first': True,
        }

        self._stats = {}

    def run_and_verify_storage_cli(self, *cmd, **kwargs):
        """Run storage CLI and return the result or raise an exception."""
        do_raise = kwargs.pop('do_raise', True)
        ignore_error = kwargs.get('ignore_error')
        success_code = kwargs.get('success_code', set([0]))
        (ret, stdout, stderr) = self.run_storage_cli(*cmd, **kwargs)
        if (ret not in success_code and
                not utils.check_ignore_error(ignore_error, stderr)):
            msg = utils.output_log(
                MSG.STORAGE_COMMAND_FAILED, cmd=utils.mask_password(cmd),
                ret=ret, out=' '.join(stdout.splitlines()),
                err=' '.join(stderr.splitlines()))
            if do_raise:
                raise exception.VSPError(msg)
        return ret, stdout, stderr

    @abc.abstractmethod
    def run_storage_cli(self, *cmd, **kwargs):
        """Run storage CLI."""
        raise NotImplementedError()

    def get_copy_method(self, metadata):
        """Return copy method(FULL or THIN)."""
        method = metadata.get(
            'copy_method', self.conf.vsp_default_copy_method)
        if method not in _COPY_METHOD:
            msg = utils.output_log(MSG.INVALID_PARAMETER_VALUE,
                                   meta='copy_method')
            raise exception.VSPError(msg)
        if method == 'THIN' and not self.conf.vsp_thin_pool:
            msg = utils.output_log(MSG.INVALID_PARAMETER,
                                   param='vsp_thin_pool')
            raise exception.VSPError(msg)
        return method

    def create_volume(self, volume):
        """Create a volume and return its properties."""
        try:
            ldev = self.create_ldev(volume['size'])
        except exception.VSPError:
            with excutils.save_and_reraise_exception():
                utils.output_log(MSG.CREATE_LDEV_FAILED)
        return {
            'provider_location': six.text_type(ldev),
        }

    def create_ldev(self, size, is_vvol=False):
        """Create an LDEV and return its LDEV number."""
        ldev = self.get_unused_ldev()
        self.create_ldev_on_storage(ldev, size, is_vvol)
        LOG.debug('Created logical device. (LDEV: %s)', ldev)
        return ldev

    @abc.abstractmethod
    def create_ldev_on_storage(self, ldev, size, is_vvol):
        """Create an LDEV on the storage system."""
        raise NotImplementedError()

    @abc.abstractmethod
    def get_unused_ldev(self):
        """Find an unused LDEV and return its LDEV number."""
        raise NotImplementedError()

    def create_volume_from_snapshot(self, volume, snapshot):
        """Create a volume from a snapshot and return its properties."""
        ldev = utils.get_ldev(snapshot)
        # When 'ldev' is 0, it should be true.
        # Therefore, it cannot remove 'is None'.
        if ldev is None:
            msg = utils.output_log(
                MSG.INVALID_LDEV_FOR_VOLUME_COPY, type='snapshot',
                id=snapshot['id'])
            raise exception.VSPError(msg)
        size = volume['size']
        metadata = utils.get_volume_metadata(volume)
        if size < snapshot['volume_size']:
            msg = utils.output_log(
                MSG.INVALID_VOLUME_SIZE_FOR_COPY, type='snapshot',
                volume_id=volume['id'])
            raise exception.VSPError(msg)
        elif (size > snapshot['volume_size'] and not self.check_vvol(ldev) and
              self.get_copy_method(metadata) == "THIN"):
            msg = utils.output_log(MSG.INVALID_VOLUME_SIZE_FOR_TI,
                                   copy_method=utils.THIN,
                                   type='snapshot', volume_id=volume['id'])
            raise exception.VSPError(msg)
        sync = size > snapshot['volume_size']
        new_ldev = self._copy_ldev(
            ldev, snapshot['volume_size'], metadata, sync)
        if sync:
            self.delete_pair(new_ldev)
            self.extend_ldev(new_ldev, snapshot['volume_size'], size)
        return {
            'provider_location': six.text_type(new_ldev),
        }

    def _copy_ldev(self, ldev, size, metadata, sync=False):
        """Create a copy of the specified volume and return its properties."""
        try:
            return self.copy_on_storage(ldev, size, metadata, sync)
        except exception.VSPNotSupported:
            return self._copy_on_host(ldev, size)

    def _copy_on_host(self, src_ldev, size):
        """Create a copy of the specified LDEV via host."""
        dest_ldev = self.create_ldev(size)
        try:
            self._copy_with_dd(src_ldev, dest_ldev, size)
        except Exception:
            with excutils.save_and_reraise_exception():
                try:
                    self._delete_ldev(dest_ldev)
                except exception.VSPError:
                    utils.output_log(MSG.DELETE_LDEV_FAILED, ldev=dest_ldev)
        return dest_ldev

    def _copy_with_dd(self, src_ldev, dest_ldev, size):
        """Copy the content of a volume by dd command."""
        src_info = None
        dest_info = None
        properties = cinder_utils.brick_get_connector_properties(
            multipath=self.conf.use_multipath_for_image_xfer,
            enforce_multipath=self.conf.enforce_multipath_for_image_xfer)
        try:
            dest_info = self._attach_ldev(dest_ldev, properties)
            src_info = self._attach_ldev(src_ldev, properties)
            volume_utils.copy_volume(
                src_info['device']['path'], dest_info['device']['path'],
                size * units.Ki, self.conf.volume_dd_blocksize)
        finally:
            if src_info:
                self._detach_ldev(src_info, src_ldev, properties)
            if dest_info: