# Nimble Storage, Inc. (c) 2013-2014
# 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.
"""
Volume driver for Nimble Storage.

This driver supports Nimble Storage controller CS-Series.

"""
import functools
import random
import re
import six
import string
import sys

from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import units
from six.moves import urllib
from suds import client

from cinder import exception
from cinder.i18n import _, _LE, _LI, _LW
from cinder.objects import volume
from cinder.volume.drivers.san import san
from cinder.volume import volume_types


DRIVER_VERSION = '2.0.2'
AES_256_XTS_CIPHER = 2
DEFAULT_CIPHER = 3
EXTRA_SPEC_ENCRYPTION = 'nimble:encryption'
EXTRA_SPEC_PERF_POLICY = 'nimble:perfpol-name'
EXTRA_SPEC_MULTI_INITIATOR = 'nimble:multi-initiator'
DEFAULT_PERF_POLICY_SETTING = 'default'
DEFAULT_ENCRYPTION_SETTING = 'no'
DEFAULT_MULTI_INITIATOR_SETTING = 'false'
DEFAULT_SNAP_QUOTA = sys.maxsize
VOL_EDIT_MASK = 4 + 16 + 32 + 64 + 256 + 512
MANAGE_EDIT_MASK = 1 + 262144
UNMANAGE_EDIT_MASK = 262144
AGENT_TYPE_OPENSTACK = 5
AGENT_TYPE_NONE = 1
SOAP_PORT = 5391
SM_ACL_APPLY_TO_BOTH = 3
SM_ACL_CHAP_USER_ANY = '*'
SM_SUBNET_DATA = 3
SM_SUBNET_MGMT_PLUS_DATA = 4
LUN_ID = '0'
WARN_LEVEL = 0.8

LOG = logging.getLogger(__name__)

nimble_opts = [
    cfg.StrOpt('nimble_pool_name',
               default='default',
               help='Nimble Controller pool name'),
    cfg.StrOpt('nimble_subnet_label',
               default='*',
               help='Nimble Subnet Label'), ]


CONF = cfg.CONF
CONF.register_opts(nimble_opts)


class NimbleDriverException(exception.VolumeDriverException):
    message = _("Nimble Cinder Driver exception")


class NimbleAPIException(exception.VolumeBackendAPIException):
    message = _("Unexpected response from Nimble API")


class NimbleISCSIDriver(san.SanISCSIDriver):

    """OpenStack driver to enable Nimble Controller.

    Version history:
        1.0 - Initial driver
        1.1.1 - Updated VERSION to Nimble driver version
        1.1.2 - Update snap-quota to unlimited
        2.0.0 - Added Extra Spec Capability
                Correct capacity reporting
                Added Manage/Unmanage volume support
        2.0.1 - Added multi-initiator support through extra-specs
        2.0.2 - Fixed supporting extra specs while cloning vols
    """

    VERSION = DRIVER_VERSION

    def __init__(self, *args, **kwargs):
        super(NimbleISCSIDriver, self).__init__(*args, **kwargs)
        self.APIExecutor = None
        self.group_stats = {}
        self.configuration.append_config_values(nimble_opts)

    def _check_config(self):
        """Ensure that the flags we care about are set."""
        required_config = ['san_ip', 'san_login', 'san_password']
        for attr in required_config:
            if not getattr(self.configuration, attr, None):
                raise exception.InvalidInput(reason=_('%s is not set.') %
                                             attr)

    def _get_discovery_ip(self, netconfig):
        """Get discovery ip."""
        subnet_label = self.configuration.nimble_subnet_label
        LOG.debug('subnet_label used %(netlabel)s, netconfig %(netconf)s',
                  {'netlabel': subnet_label, 'netconf': netconfig})
        ret_discovery_ip = ''
        for subnet in netconfig['subnet-list']:
            LOG.info(_LI('Exploring array subnet label %s'), subnet['label'])
            if subnet_label == '*':
                # Use the first data subnet, save mgmt+data for later
                if subnet['subnet-id']['type'] == SM_SUBNET_DATA:
                    LOG.info(_LI('Discovery ip %(disc_ip)s is used '
                                 'on data subnet %(net_label)s'),
                             {'disc_ip': subnet['discovery-ip'],
                              'net_label': subnet['label']})
                    return subnet['discovery-ip']
                elif (subnet['subnet-id']['type'] ==
                        SM_SUBNET_MGMT_PLUS_DATA):
                    LOG.info(_LI('Discovery ip %(disc_ip)s is found'
                                 ' on mgmt+data subnet %(net_label)s'),
                             {'disc_ip': subnet['discovery-ip'],
                              'net_label': subnet['label']})
                    ret_discovery_ip = subnet['discovery-ip']
            # If subnet is specified and found, use the subnet
            elif subnet_label == subnet['label']:
                LOG.info(_LI('Discovery ip %(disc_ip)s is used'
                             ' on subnet %(net_label)s'),
                         {'disc_ip': subnet['discovery-ip'],
                          'net_label': subnet['label']})
                return subnet['discovery-ip']
        if ret_discovery_ip:
            LOG.info(_LI('Discovery ip %s is used on mgmt+data subnet'),
                     ret_discovery_ip)
            return ret_discovery_ip
        else:
            raise NimbleDriverException(_('No suitable discovery ip found'))

    def _update_existing_vols_agent_type(self, context):
        LOG.debug("Updating existing volumes to have "
                  "agent_type = 'OPENSTACK'")
        backend_name = self.configuration.safe_get('volume_backend_name')
        all_vols = volume.VolumeList.get_all(
            context, None, None, None, None, {'status': 'available'})
        for vol in all_vols:
            if backend_name in vol.host:
                try:
                    self.APIExecutor.edit_vol(
                        vol.name,
                        UNMANAGE_EDIT_MASK,
                        {'agent-type': AGENT_TYPE_OPENSTACK})
                except NimbleAPIException:
                    LOG.warning(_LW('Error updating agent-type for '
                                    'volume %s.'), vol.name)

    def do_setup(self, context):
        """Setup the Nimble Cinder volume driver."""
        self._check_config()
        # Setup API Executor
        try:
            self.APIExecutor = NimbleAPIExecutor(
                username=self.configuration.san_login,
                password=self.configuration.san_password,
                ip=self.configuration.san_ip)
        except Exception:
            LOG.error(_LE('Failed to create SOAP client.'
                          'Check san_ip, username, password'
                          ' and make sure the array version is compatible'))
            raise
        self._update_existing_vols_agent_type(context)

    def _get_provider_location(self, volume_name):
        """Get volume iqn for initiator access."""
        vol_info = self.APIExecutor.get_vol_info(volume_name)
        iqn = vol_info['target-name']
        netconfig = self.APIExecutor.get_netconfig('active')
        target_ipaddr = self._get_discovery_ip(netconfig)
        iscsi_portal = target_ipaddr + ':3260'
        provider_location = '%s %s %s' % (iscsi_portal, iqn, LUN_ID)
        LOG.info(_LI('vol_name=%(name)s provider_location=%(loc)s'),
                 {'name': volume_name, 'loc': provider_location})
        return provider_location

    def _get_model_info(self, volume_name):
        """Get model info for the volume."""
        return (
            {'provider_location': self._get_provider_location(volume_name),
             'provider_auth': None})

    def create_volume(self, volume):
        """Create a new volume."""
        reserve = not self.configuration.san_thin_provision
        self.APIExecutor.create_vol(
            volume,
            self.configuration.nimble_pool_name, reserve)
        return self._get_model_info(volume['name'])

    def delete_volume(self, volume):
        """Delete the specified volume."""
        self.APIExecutor.online_vol(volume['name'], False,
                                    ignore_list=['SM-enoent'])
        self.APIExecutor.dissociate_volcoll(volume['name'],
                                            ignore_list=['SM-enoent'])
        self.APIExecutor.delete_vol(volume['name'], ignore_list=['SM-enoent'])

    def _generate_random_string(self, length):
        """Generates random_string."""
        char_set = string.ascii_lowercase
        return ''.join(random.sample(char_set, length))

    def _clone_volume_from_snapshot(self, volume, snapshot):
        """Clone volume from snapshot.

        Extend the volume if the size of the volume is more than the snapshot.
        """
        reserve = not self.configuration.san_thin_provision
        self.APIExecutor.clone_vol(volume, snapshot, reserve)
        if(volume['size'] > snapshot['volume_size']):
            vol_size = volume['size'] * units.Gi
            reserve_size = vol_size if reserve else 0
            self.APIExecutor.edit_vol(
                volume['name'],
                VOL_EDIT_MASK,  # mask for vol attributes
                {'size': vol_size,
                 'reserve': reserve_size,
                 'warn-level': int(vol_size * WARN_LEVEL),
                 'quota': vol_size,
                 'snap-quota': DEFAULT_SNAP_QUOTA})
        return self._get_model_info(volume['name'])

    def create_cloned_volume(self, volume, src_vref):
        """Create a clone of the specified volume."""
        snapshot_name = ('openstack-clone-' +
                         volume['name'] + '-' +
                         self._generate_random_string(12))
        snapshot = {'volume_name': src_vref['name'],
                    'name': snapshot_name,
                    'volume_size': src_vref['size'],
                    'display_name': '',
                    'display_description': ''}
        self.APIExecutor.snap_vol(snapshot)
        self._clone_volume_from_snapshot(volume, snapshot)
        return self._get_model_info(volume['name'])

    def create_export(self, context, volume, connector):
        """Driver entry point to get the export info for a new volume."""
        return self._get_model_info(volume['name'])

    def ensure_export(self, context, volume):
        """Driver entry point to get the export info for an existing volume."""
        return self._get_model_info(volume['name'])

    def create_snapshot(self, snapshot):
        """Create a snapshot."""
        self.APIExecutor.snap_vol(snapshot)

    def delete_snapshot(self, snapshot):
        """Delete a snapshot."""
        self.APIExecutor.online_snap(
            snapshot['volume_name'],
            False,
            snapshot['name'],
            ignore_list=['SM-ealready', 'SM-enoent'])
        self.APIExecutor.delete_snap(snapshot['volume_name'],
                                     snapshot['name'],
                                     ignore_list=['SM-enoent'])

    def create_volume_from_snapshot(self, volume, snapshot):
        """Create a volume from a snapshot."""
        self._clone_volume_from_snapshot(volume, snapshot)
        return self._get_model_info(volume['name'])

    def get_volume_stats(self, refresh=False):
        """Get volume stats. This is more of getting group stats."""
        if refresh:
            group_info = self.APIExecutor.get_group_config()
            if not group_info['spaceInfoValid']:
                raise NimbleDriverException(_('SpaceInfo returned by'
                                              'array is invalid'))
            total_capacity = (group_info['usableCapacity'] /
                              float(units.Gi))
            used_space = ((group_info['volUsageCompressed'] +
                          group_info['snapUsageCompressed'] +
                          group_info['unusedReserve']) /
                          float(units.Gi))
            free_space = total_capacity - used_space
            LOG.debug('total_capacity=%(capacity)f '
                      'used_space=%(used)f free_space=%(free)f',
                      {'capacity': total_capacity,
                       'used': used_space,
                       'free': free_space})
            backend_name = self.configuration.safe_get(
                'volume_backend_name') or self.__class__.__name__
            self.group_stats = {'volume_backend_name': backend_name,
                                'vendor_name': 'Nimble',
                                'driver_version': DRIVER_VERSION,
                                'storage_protocol': 'iSCSI'}
            # Just use a single pool for now, FIXME to support multiple
            # pools
            single_pool = dict(
                pool_name=backend_name,
                total_capacity_gb=total_capacity,
                free_capacity_gb=free_space,
                reserved_percentage=0,
                QoS_support=False)
            self.group_stats['pools'] = [single_pool]
        return self.group_stats

    def extend_volume(self, volume, new_size):
        """Extend an existing volume."""
        volume_name = volume['name']
        LOG.info(_LI('Entering extend_volume volume=%(vol)s '
                     'new_size=%(size)s'),
                 {'vol': volume_name, 'size': new_size})
        vol_size = int(new_size) * units.Gi
        reserve = not self.configuration.san_thin_provision
        reserve_size = vol_size if reserve else 0
        self.APIExecutor.edit_vol(
            volume_name,
            VOL_EDIT_MASK,  # mask for vol attributes
            {'size': vol_size,
             'reserve': reserve_size,
             'warn-level': int(vol_size * WARN_LEVEL),
             'quota': vol_size,
             'snap-quota': DEFAULT_SNAP_QUOTA})

    def _get_existing_volume_ref_name(self, existing_ref):
        """Returns the volume name of an existing ref"""

        vol_name = None
        if 'source-name' in existing_ref:
            vol_name = existing_ref['source-name']
        else:
            reason = _("Reference must contain source-name.")
            raise exception.ManageExistingInvalidReference(
                existing_ref=existing_ref,
                reason=reason)

        return vol_name

    def manage_existing(self, volume, external_ref):
        """Manage an existing nimble volume (import to cinder)"""

        # Get the volume name from the external reference
        target_vol_name = self._get_existing_volume_ref_name(external_ref)
        LOG.debug('Entering manage_existing. '
                  'Target_volume_name = %s', target_vol_name)

        # Get vol info from the volume name obtained from the reference
        vol_info = self.APIExecutor.get_vol_info(target_vol_name)

        # Check if volume is already managed by OpenStack
        if vol_info['agent-type'] == AGENT_TYPE_OPENSTACK:
            msg = (_('Volume %s is already managed by OpenStack.')
                   % target_vol_name)
            raise exception.ManageExistingAlreadyManaged(
                volume_ref=volume['id'])

        # If agent-type is not None then raise exception
        if vol_info['agent-type'] != AGENT_TYPE_NONE:
            msg = (_('Volume should have agent-type set as None.'))
            raise exception.InvalidVolume(reason=msg)

        new_vol_name = volume['name']

        if vol_info['online']:
            msg = (_('Volume %s is online. Set volume to offline for '
                     'managing using OpenStack.') % target_vol_name)
            raise exception.InvalidVolume(reason=msg)

        # edit the volume
        self.APIExecutor.edit_vol(target_vol_name,
                                  MANAGE_EDIT_MASK,
                                  {'name': new_vol_name,
                                   'agent-type': AGENT_TYPE_OPENSTACK})

        # make the volume online after rename
        self.APIExecutor.online_vol(new_vol_name, True, ignore_list=[
            'SM-enoent'])

        return self._get_model_info(new_vol_name)

    def manage_existing_get_size(self, volume, external_ref):
        """Return size of an existing volume"""

        LOG.debug('Volume name : %(name)s  External ref : %(ref)s',
                  {'name': volume['name'], 'ref': external_ref})

        target_vol_name = self._get_existing_volume_ref_name(external_ref)

        # get vol info
        vol_info = self.APIExecutor.get_vol_info(target_vol_name)

        LOG.debug('Volume size : %(size)s  Volume-name : %(name)s',
                  {'size': vol_info['size'], 'name': vol_info['name']})

        return int(vol_info['size'] / units.Gi)

    def unmanage(self, volume):
        """Removes the specified volume from Cinder management."""

        vol_name = volume['name']
        LOG.info(_LI("Entering unmanage_volume volume = %s"), vol_name)

        # check agent type
        vol_info = self.APIExecutor.get_vol_info(vol_name)
        if vol_info['agent-type'] != AGENT_TYPE_OPENSTACK:
            msg = (_('Only volumes managed by OpenStack can be unmanaged.'))
            raise exception.InvalidVolume(reason=msg)

        # update the agent-type to None
        self.APIExecutor.edit_vol(vol_name,
                                  UNMANAGE_EDIT_MASK,
                                  {'agent-type': AGENT_TYPE_NONE})

        # offline the volume
        self.APIExecutor.online_vol(vol_name, False, ignore_list=[
            'SM-enoent'])

    def _create_igroup_for_initiator(self, initiator_name):
        """Creates igroup for an initiator and returns the igroup name."""
        igrp_name = 'openstack-' + self._generate_random_string(12)
        LOG.info(_LI('Creating initiator group %(grp)s '
                     'with initiator %(iname)s'),
                 {'grp': igrp_name, 'iname': initiator_name})
        self.APIExecutor.create_initiator_group(igrp_name, initiator_name)
        return igrp_name

    def _get_igroupname_for_initiator(self, initiator_name):
        initiator_groups = self.APIExecutor.get_initiator_grp_list()
        for initiator_group in initiator_groups:
            if 'initiator-list' in initiator_group:
                if (len(initiator_group['initiator-list']) == 1 and
                    initiator_group['initiator-list'][0]['name'] ==
                        initiator_name):
                    LOG.info(_LI('igroup %(grp)s found for '
                                 'initiator %(iname)s'),
                             {'grp': initiator_group['name'],
                              'iname': initiator_name})
                    return initiator_group['name']
        LOG.info(_LI('No igroup found for initiator %s'), initiator_name)
        return ''

    def initialize_connection(self, volume, connector):
        """Driver entry point to attach a volume to an instance."""
        LOG.info(_LI('Entering initialize_connection volume=%(vol)s'
                     ' connector=%(conn)s location=%(loc)s'),
                 {'vol': volume,
                  'conn': connector,
                  'loc': volume['provider_location']})
        initiator_name = connector['initiator']
        initiator_group_name = self._get_igroupname_for_initiator(
            initiator_name)
        if not initiator_group_name:
            initiator_group_name = self._create_igroup_for_initiator(
                initiator_name)
        LOG.info(_LI('Initiator group name is %(grp)s for initiator '
                     '%(iname)s'),
                 {'grp': initiator_group_name, 'iname': initiator_name})
        self.APIExecutor.add_acl(volume, initiator_group_name)
        (iscsi_portal, iqn, lun_num) = volume['provider_location'].split()
        properties = {}
        properties['target_discovered'] = False  # whether discovery was used
        properties['target_portal'] = iscsi_portal
        properties['target_iqn'] = iqn
        properties['target_lun'] = lun_num
        properties['volume_id'] = volume['id']  # used by xen currently
        return {
            'driver_volume_type': 'iscsi',
            'data': properties,
        }

    def terminate_connection(self, volume, connector, **kwargs):
        """Driver entry point to unattach a volume from an instance."""
        LOG.info(_LI('Entering terminate_connection volume=%(vol)s'
                     ' connector=%(conn)s location=%(loc)s.'),
                 {'vol': volume,
                  'conn': connector,
                  'loc': volume['provider_location']})
        initiator_name = connector['initiator']
        initiator_group_name = self._get_igroupname_for_initiator(
            initiator_name)
        if not initiator_group_name:
            raise NimbleDriverException(
                _('No initiator group found for initiator %s') %
                initiator_name)
        self.APIExecutor.remove_acl(volume, initiator_group_name)


def _response_checker(func):
    """Decorator function to check if the response of an API is positive."""
    @functools.wraps(func)
    def inner_response_checker(self, *args, **kwargs):
        response = func(self, *args, **kwargs)
        ignore_list = (kwargs['ignore_list']
                       if 'ignore_list' in kwargs else [])
        for err in response['err-list']['err-list']:
            err_str = self._get_err_str(err['code'])
            if err_str != 'SM-ok' and err_str not in ignore_list:
                msg = (_('API %(name)s failed with error string %(err)s')
                       % {'name': func.__name__, 'err': err_str})
                LOG.error(msg)
                raise NimbleAPIException(msg)
            return response
    return inner_response_checker


def _connection_checker(func):
    """Decorator to re-establish and re-run the api if session has expired."""
    @functools.wraps(func)
    def inner_connection_checker(self, *args, **kwargs):
        for attempts in range(2):
            try:
                return func(self, *args, **kwargs)
            except NimbleAPIException as e:
                if attempts < 1 and (re.search('SM-eaccess',
                                     six.text_type(e))):
                    LOG.info(_LI('Session might have expired.'
                                 ' Trying to relogin'))
                    self.login()
                    continue
                else:
                    LOG.error(_LE('Re-throwing Exception %s'), e)
                    raise
    return inner_connection_checker


class NimbleAPIExecutor(object):

    """Makes Nimble API calls."""

    def __init__(self, *args, **kwargs):
        self.sid = None
        self.username = kwargs['username']
        self.password = kwargs['password']

        wsdl_url = 'https://%s/wsdl/NsGroupManagement.wsdl' % (kwargs['ip'])
        LOG.debug('Using Nimble wsdl_url: %s', wsdl_url)
        self.err_string_dict = self._create_err_code_to_str_mapper(wsdl_url)
        self.client = client.Client(wsdl_url,
                                    username=self.username,
                                    password=self.password)
        soap_url = ('https://%(ip)s:%(port)s/soap' % {'ip': kwargs['ip'],
                                                      'port': SOAP_PORT})
        LOG.debug('Using Nimble soap_url: %s', soap_url)
        self.client.set_options(location=soap_url)
        self.login()

    def _create_err_code_to_str_mapper(self, wsdl_url):
        f = urllib.request.urlopen(wsdl_url)
        wsdl_file = f.read()
        err_enums = re.findall(
            r'<simpleType name="SmErrorType">(.*?)</simpleType>',
            wsdl_file,
            re.DOTALL)
        err_enums = ''.join(err_enums).split('\n')
        ret_dict = {}
        for enum in err_enums:
            m = re.search(r'"(.*?)"(.*?)= (\d+) ', enum)
            if m:
                ret_dict[int(m.group(3))] = m.group(1)
        return ret_dict

    def _get_err_str(self, code):
        if code in self.err_string_dict:
            return self.err_string_dict[code]
        else:
            return 'Unknown error Code: %s' % code

    @_response_checker
    def _execute_login(self):
        return self.client.service.login(req={
            'username': self.username,
            'password': self.password
        })

    def login(self):
        """Execute Https Login API."""
        response = self._execute_login()
        LOG.info(_LI('Successful login by user %s'), self.username)
        self.sid = response['authInfo']['sid']

    @_connection_checker
    @_response_checker
    def _execute_get_netconfig(self, name):
        return self.client.service.getNetConfig(request={'sid': self.sid,
                                                         'name': name})

    def get_netconfig(self, name):
        """Execute getNetConfig API."""
        response = self._execute_get_netconfig(name)
        return response['config']

    def _get_volumetype_extraspecs(self, volume):
        specs = {}

        type_id = volume['volume_type_id']
        if type_id is not None:
            specs = volume_types.get_volume_type_extra_specs(type_id)
        return specs

    def _get_extra_spec_values(self, extra_specs):
        """Nimble specific extra specs."""
        perf_policy_name = extra_specs.get(EXTRA_SPEC_PERF_POLICY,
                                           DEFAULT_PERF_POLICY_SETTING)
        encryption = extra_specs.get(EXTRA_SPEC_ENCRYPTION,
                                     DEFAULT_ENCRYPTION_SETTING)
        multi_initiator = extra_specs.get(EXTRA_SPEC_MULTI_INITIATOR,
                                          DEFAULT_MULTI_INITIATOR_SETTING)
        extra_specs_map = {}
        extra_specs_map[EXTRA_SPEC_PERF_POLICY] = perf_policy_name
        extra_specs_map[EXTRA_SPEC_ENCRYPTION] = encryption
        extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR] = multi_initiator

        return extra_specs_map

    @_connection_checker
    @_response_checker
    def _execute_create_vol(self, volume, pool_name, reserve):
        # Set volume size, display name and description
        volume_size = volume['size'] * units.Gi
        reserve_size = volume_size if reserve else 0
        # Set volume description
        display_list = [getattr(volume, 'display_name', ''),
                        getattr(volume, 'display_description', '')]
        description = ':'.join(filter(None, display_list))
        # Limit description size to 254 characters
        description = description[:254]

        specs = self._get_volumetype_extraspecs(volume)
        extra_specs_map = self._get_extra_spec_values(specs)
        perf_policy_name = extra_specs_map[EXTRA_SPEC_PERF_POLICY]
        encrypt = extra_specs_map[EXTRA_SPEC_ENCRYPTION]
        multi_initiator = extra_specs_map[EXTRA_SPEC_MULTI_INITIATOR]
        # default value of cipher for encryption
        cipher = DEFAULT_CIPHER
        if encrypt.lower() == 'yes':
            cipher = AES_256_XTS_CIPHER

        LOG.debug('Creating a new volume=%(vol)s size=%(size)s'
                  ' reserve=%(reserve)s in pool=%(pool)s'
                  ' description=%(description)s with Extra Specs'
                  ' perfpol-name=%(perfpol-name)s'
                  ' encryption=%(encryption)s cipher=%(cipher)s'
                  ' agent-type=%(agent-type)s'
                  ' multi-initiator=%(multi-initiator)s',
                  {'vol': volume['name'],
                   'size': volume_size,
                   'reserve': reserve,
                   'pool': pool_name,
                   'description': description,
                   'perfpol-name': perf_policy_name,
                   'encryption': encrypt,
                   'cipher': cipher,
                   'agent-type': AGENT_TYPE_OPENSTACK,
                   'multi-initiator': multi_initiator})

        return self.client.service.createVol(
            request={'sid': self.sid,
                     'attr': {'name': volume['name'],
                              'description': description,
                              'size': volume_size,
                              'reserve': reserve_size,
                              'warn-level': int(volume_size * WARN_LEVEL),
                              'quota': volume_size,
                              'snap-quota': DEFAULT_SNAP_QUOTA,
                              'online': True,
                              'pool-name': pool_name,
                              'agent-type': AGENT_TYPE_OPENSTACK,
                              'perfpol-name': perf_policy_name,
                              'encryptionAttr': {'cipher': cipher},
                              'multi-initiator': multi_initiator}})

    def create_vol(self, volume, pool_name, reserve):
        """Execute createVol API."""
        response = self._execute_create_vol(volume, pool_name, reserve)
        LOG.info(_LI('Successfully create volume %s'), response['name'])
        return response['name']

    @_connection_checker
    @_response_checker
    def _execute_get_group_config(self):
        LOG.debug('Getting group config information')
        return self.client.service.getGroupConfig(request={'sid': self.sid})

    def get_group_config(self):
        """Execute getGroupConfig API."""
        response = self._execute_get_group_config()
        LOG.debug('Successfully retrieved group config information')
        return response['info']

    @_connection_checker
    @_response_checker
    def add_acl(self, volume, initiator_group_name):
        """Execute addAcl API."""
        LOG.info(_LI('Adding ACL to volume=%(vol)s with'
                     ' initiator group name %(igrp)s'),
                 {'vol': volume['name'],
                  'igrp': initiator_group_name})
        return self.client.service.addVolAcl(
            request={'sid': self.sid,
                     'volname': volume['name'],
                     'apply-to': SM_ACL_APPLY_TO_BOTH,
                     'chapuser': SM_ACL_CHAP_USER_ANY,
                     'initiatorgrp': initiator_group_name})

    @_connection_checker
    @_response_checker
    def remove_acl(self, volume, initiator_group_name):
        """Execute removeVolAcl API."""
        LOG.info(_LI('Removing ACL from volume=%(vol)s'
                     ' for initiator group %(igrp)s'),
                 {'vol': volume['name'],
                  'igrp': initiator_group_name})
        return self.client.service.removeVolAcl(
            request={'sid': self.sid,
                     'volname': volume['name'],
                     'apply-to': SM_ACL_APPLY_TO_BOTH,
                     'chapuser': SM_ACL_CHAP_USER_ANY,
                     'initiatorgrp': initiator_group_name})

    @_connection_checker
    @_response_checker
    def _execute_get_vol_info(self, vol_name):
        LOG.info(_LI('Getting volume information '
                     'for vol_name=%s'), vol_name)
        return self.client.service.getVolInfo(request={'sid': self.sid,
                                                       'name': vol_name})

    def get_vol_info(self, vol_name):
        """Execute getVolInfo API."""
        response = self._execute_get_vol_info(vol_name)
        LOG.info(_LI('Successfully got volume information for volume %s'),
                 vol_name)
        return response['vol']

    @_connection_checker
    @_response_checker
    def online_vol(self, vol_name, online_flag, *args, **kwargs):
        """Execute onlineVol API."""
        LOG.info(_LI('Setting volume %(vol)s to online_flag %(flag)s'),
                 {'vol': vol_name, 'flag': online_flag})
        return self.client.service.onlineVol(request={'sid': self.sid,
                                                      'name': vol_name,
                                                      'online': online_flag})

    @_connection_checker
    @_response_checker
    def online_snap(self, vol_name, online_flag, snap_name, *args, **kwargs):
        """Execute onlineSnap API."""
        LOG.info(_LI('Setting snapshot %(snap)s to online_flag %(flag)s'),
                 {'snap': snap_name, 'flag': online_flag})
        return self.client.service.onlineSnap(request={'sid': self.sid,
                                                       'vol': vol_name,
                                                       'name': snap_name,
                                                       'online': online_flag})

    @_connection_checker
    @_response_checker
    def dissociate_volcoll(self, vol_name, *args, **kwargs):
        """Execute dissocProtPol API."""
        LOG.info(_LI('Dissociating volume %s '), vol_name)
        return self.client.service.dissocProtPol(
            request={'sid': self.sid,
                     'vol-name': vol_name})

    @_connection_checker
    @_response_checker
    def delete_vol(self, vol_name, *args, **kwargs):
        """Execute deleteVol API."""
        LOG.info(_LI('Deleting volume %s '), vol_name)
        return self.client.service.deleteVol(request={'sid': self.sid,
                                                      'name': vol_name})

    @_connection_checker
    @_response_checker
    def snap_vol(self, snapshot):
        """Execute snapVol API."""
        volume_name = snapshot['volume_name']
        snap_name = snapshot['name']
        # Set snapshot description
        display_list = [getattr(snapshot, 'display_name', ''),
                        getattr(snapshot, 'display_description', '')]
        snap_description = ':'.join(filter(None, display_list))
        # Limit to 254 characters
        snap_description = snap_description[:254]
        LOG.info(_LI('Creating snapshot for volume_name=%(vol)s'
                     ' snap_name=%(name)s snap_description=%(desc)s'),
                 {'vol': volume_name,
                  'name': snap_name,
                  'desc': snap_description})
        return self.client.service.snapVol(
            request={'sid': self.sid,
                     'vol': volume_name,
                     'snapAttr': {'name': snap_name,
                                  'description': snap_description}})

    @_connection_checker
    @_response_checker
    def delete_snap(self, vol_name, snap_name, *args, **kwargs):
        """Execute deleteSnap API."""
        LOG.info(_LI('Deleting snapshot %s '), snap_name)
        return self.client.service.deleteSnap(request={'sid': self.sid,
                                                       'vol': vol_name,
                                                       'name': snap_name})

    @_connection_checker
    @_response_checker
    def clone_vol(self, volume, snapshot, reserve):
        """Execute cloneVol API."""
        volume_name = snapshot['volume_name']
        snap_name = snapshot['name']
        clone_name = volume['name']
        snap_size = snapshot['volume_size']
        reserve_size = snap_size * units.Gi if reserve else 0

        specs = self._get_volumetype_extraspecs(volume)
        extra_specs_map = self._get_extra_spec_values(specs)
        perf_policy_name = extra_specs_map.get(EXTRA_SPEC_PERF_POLICY)
        encrypt = extra_specs_map.get(EXTRA_SPEC_ENCRYPTION)
        multi_initiator = extra_specs_map.get(EXTRA_SPEC_MULTI_INITIATOR)
        # default value of cipher for encryption
        cipher = DEFAULT_CIPHER
        if encrypt.lower() == 'yes':
            cipher = AES_256_XTS_CIPHER

        LOG.info(_LI('Cloning volume from snapshot volume=%(vol)s '
                     'snapshot=%(snap)s clone=%(clone)s snap_size=%(size)s '
                     'reserve=%(reserve)s' 'agent-type=%(agent-type)s '
                     'perfpol-name=%(perfpol-name)s '
                     'encryption=%(encryption)s cipher=%(cipher)s '
                     'multi-initiator=%(multi-initiator)s'),
                 {'vol': volume_name,
                  'snap': snap_name,
                  'clone': clone_name,
                  'size': snap_size,
                  'reserve': reserve,
                  'agent-type': AGENT_TYPE_OPENSTACK,
                  'perfpol-name': perf_policy_name,
                  'encryption': encrypt,
                  'cipher': cipher,
                  'multi-initiator': multi_initiator})
        clone_size = snap_size * units.Gi
        return self.client.service.cloneVol(
            request={'sid': self.sid,
                     'name': volume_name,
                     'attr': {'name': clone_name,
                              'reserve': reserve_size,
                              'warn-level': int(clone_size * WARN_LEVEL),
                              'quota': clone_size,
                              'snap-quota': DEFAULT_SNAP_QUOTA,
                              'online': True,
                              'agent-type': AGENT_TYPE_OPENSTACK,
                              'perfpol-name': perf_policy_name,
                              'encryptionAttr': {'cipher': cipher},
                              'multi-initiator': multi_initiator},
                     'snap-name': snap_name})

    @_connection_checker
    @_response_checker
    def edit_vol(self, vol_name, mask, attr):
        """Execute editVol API."""
        LOG.info(_LI('Editing Volume %(vol)s with mask %(mask)s'),
                 {'vol': vol_name, 'mask': str(mask)})
        return self.client.service.editVol(request={'sid': self.sid,
                                                    'name': vol_name,
                                                    'mask': mask,
                                                    'attr': attr})

    @_connection_checker
    @_response_checker
    def _execute_get_initiator_grp_list(self):
        LOG.info(_LI('Getting getInitiatorGrpList'))
        return (self.client.service.getInitiatorGrpList(
            request={'sid': self.sid}))

    def get_initiator_grp_list(self):
        """Execute getInitiatorGrpList API."""
        response = self._execute_get_initiator_grp_list()
        LOG.info(_LI('Successfully retrieved InitiatorGrpList'))
        return (response['initiatorgrp-list']
                if 'initiatorgrp-list' in response else [])

    @_connection_checker
    @_response_checker
    def create_initiator_group(self, initiator_group_name, initiator_name):
        """Execute createInitiatorGrp API."""
        LOG.info(_LI('Creating initiator group %(igrp)s'
                     ' with one initiator %(iname)s'),
                 {'igrp': initiator_group_name, 'iname': initiator_name})
        return self.client.service.createInitiatorGrp(
            request={'sid': self.sid,
                     'attr': {'name': initiator_group_name,
                              'initiator-list': [{'label': initiator_name,
                                                  'name': initiator_name}]}})

    @_connection_checker
    @_response_checker
    def delete_initiator_group(self, initiator_group_name, *args, **kwargs):
        """Execute deleteInitiatorGrp API."""
        LOG.info(_LI('Deleting deleteInitiatorGrp %s '), initiator_group_name)
        return self.client.service.deleteInitiatorGrp(
            request={'sid': self.sid,
                     'name': initiator_group_name})
