# Copyright 2015 Hewlett Packard Enterprise Development LP
#
#    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.

"""HPE 3PAR Mediator for OpenStack Manila.

This 'mediator' de-couples the 3PAR focused client from the OpenStack focused
driver.
"""

from oslo_log import log
from oslo_utils import importutils
from oslo_utils import units

from manila.data import utils as data_utils
from manila import exception
from manila.i18n import _
from manila import utils

hpe3parclient = importutils.try_import("hpe3parclient")
if hpe3parclient:
    from hpe3parclient import file_client  # pylint: disable=import-error


LOG = log.getLogger(__name__)
MIN_CLIENT_VERSION = (4, 0, 0)
DENY = '-'
ALLOW = '+'
OPEN_STACK_MANILA = 'OpenStack Manila'
FULL = 1
THIN = 2
DEDUPE = 6
ENABLED = 1
DISABLED = 2
CACHE = 'cache'
CONTINUOUS_AVAIL = 'continuous_avail'
ACCESS_BASED_ENUM = 'access_based_enum'
SMB_EXTRA_SPECS_MAP = {
    CACHE: CACHE,
    CONTINUOUS_AVAIL: 'ca',
    ACCESS_BASED_ENUM: 'abe',
}
IP_ALREADY_EXISTS = 'IP address %s already exists'
USER_ALREADY_EXISTS = '"allow" permission already exists for "%s"'
DOES_NOT_EXIST = 'does not exist, cannot'
LOCAL_IP = '127.0.0.1'
LOCAL_IP_RO = '127.0.0.2'
SUPER_SHARE = 'OPENSTACK_SUPER_SHARE'
TMP_RO_SNAP_EXPORT = "Temp RO snapshot export as source for creating RW share."


class HPE3ParMediator(object):
    """3PAR client-facing code for the 3PAR driver.

    Version history:
        1.0.0 - Begin Liberty development (post-Kilo)
        1.0.1 - Report thin/dedup/hp_flash_cache capabilities
        1.0.2 - Add share server/share network support
        1.0.3 - Use hp3par prefix for share types and capabilities
        2.0.0 - Rebranded HP to HPE
        2.0.1 - Add access_level (e.g. read-only support)
        2.0.2 - Add extend/shrink
        2.0.3 - Fix SMB read-only access (added in 2.0.1)
        2.0.4 - Remove file tree on delete when using nested shares #1538800
        2.0.5 - Reduce the fsquota by share size
                when a share is deleted #1582931
        2.0.6 - Read-write share from snapshot (using driver mount and copy)
        2.0.7 - Add update_access support
        2.0.8 - Multi pools support per backend
        2.0.9 - Fix get_vfs() to correctly validate conf IP addresses at
                boot up #1621016

    """

    VERSION = "2.0.9"

    def __init__(self, **kwargs):

        self.hpe3par_username = kwargs.get('hpe3par_username')
        self.hpe3par_password = kwargs.get('hpe3par_password')
        self.hpe3par_api_url = kwargs.get('hpe3par_api_url')
        self.hpe3par_debug = kwargs.get('hpe3par_debug')
        self.hpe3par_san_ip = kwargs.get('hpe3par_san_ip')
        self.hpe3par_san_login = kwargs.get('hpe3par_san_login')
        self.hpe3par_san_password = kwargs.get('hpe3par_san_password')
        self.hpe3par_san_ssh_port = kwargs.get('hpe3par_san_ssh_port')
        self.hpe3par_san_private_key = kwargs.get('hpe3par_san_private_key')
        self.hpe3par_fstore_per_share = kwargs.get('hpe3par_fstore_per_share')
        self.hpe3par_require_cifs_ip = kwargs.get('hpe3par_require_cifs_ip')
        self.hpe3par_cifs_admin_access_username = (
            kwargs.get('hpe3par_cifs_admin_access_username'))
        self.hpe3par_cifs_admin_access_password = (
            kwargs.get('hpe3par_cifs_admin_access_password'))
        self.hpe3par_cifs_admin_access_domain = (
            kwargs.get('hpe3par_cifs_admin_access_domain'))
        self.hpe3par_share_mount_path = kwargs.get('hpe3par_share_mount_path')
        self.my_ip = kwargs.get('my_ip')

        self.ssh_conn_timeout = kwargs.get('ssh_conn_timeout')
        self._client = None
        self.client_version = None

    @staticmethod
    def no_client():
        return hpe3parclient is None

    def do_setup(self):

        if self.no_client():
            msg = _('You must install hpe3parclient before using the 3PAR '
                    'driver. Run "pip install --upgrade python-3parclient" '
                    'to upgrade the hpe3parclient.')
            LOG.error(msg)
            raise exception.HPE3ParInvalidClient(message=msg)

        self.client_version = hpe3parclient.version_tuple
        if self.client_version < MIN_CLIENT_VERSION:
            msg = (_('Invalid hpe3parclient version found (%(found)s). '
                     'Version %(minimum)s or greater required. Run "pip'
                     ' install --upgrade python-3parclient" to upgrade'
                     ' the hpe3parclient.') %
                   {'found': '.'.join(map(str, self.client_version)),
                    'minimum': '.'.join(map(str,
                                            MIN_CLIENT_VERSION))})
            LOG.error(msg)
            raise exception.HPE3ParInvalidClient(message=msg)

        try:
            self._client = file_client.HPE3ParFilePersonaClient(
                self.hpe3par_api_url)
        except Exception as e:
            msg = (_('Failed to connect to HPE 3PAR File Persona Client: %s') %
                   str(e))
            LOG.exception(msg)
            raise exception.ShareBackendException(message=msg)

        try:
            ssh_kwargs = {}
            if self.hpe3par_san_ssh_port:
                ssh_kwargs['port'] = self.hpe3par_san_ssh_port
            if self.ssh_conn_timeout:
                ssh_kwargs['conn_timeout'] = self.ssh_conn_timeout
            if self.hpe3par_san_private_key:
                ssh_kwargs['privatekey'] = self.hpe3par_san_private_key

            self._client.setSSHOptions(
                self.hpe3par_san_ip,
                self.hpe3par_san_login,
                self.hpe3par_san_password,
                **ssh_kwargs
            )

        except Exception as e:
            msg = (_('Failed to set SSH options for HPE 3PAR File Persona '
                     'Client: %s') % str(e))
            LOG.exception(msg)
            raise exception.ShareBackendException(message=msg)

        LOG.info("HPE3ParMediator %(version)s, "
                 "hpe3parclient %(client_version)s",
                 {"version": self.VERSION,
                  "client_version": hpe3parclient.get_version_string()})

        try:
            wsapi_version = self._client.getWsApiVersion()['build']
            LOG.info("3PAR WSAPI %s", wsapi_version)
        except Exception as e:
            msg = (_('Failed to get 3PAR WSAPI version: %s') %
                   str(e))
            LOG.exception(msg)
            raise exception.ShareBackendException(message=msg)

        if self.hpe3par_debug:
            self._client.debug_rest(True)  # Includes SSH debug (setSSH above)

    def _wsapi_login(self):
        try:
            self._client.login(self.hpe3par_username, self.hpe3par_password)
        except Exception as e:
            msg = (_("Failed to Login to 3PAR (%(url)s) as %(user)s "
                     "because: %(err)s") %
                   {'url': self.hpe3par_api_url,
                    'user': self.hpe3par_username,
                    'err': str(e)})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

    def _wsapi_logout(self):
        try:
            self._client.http.unauthenticate()
        except Exception as e:
            msg = ("Failed to Logout from 3PAR (%(url)s) because %(err)s")
            LOG.warning(msg, {'url': self.hpe3par_api_url,
                              'err': str(e)})
            # don't raise exception on logout()

    @staticmethod
    def build_export_locations(protocol, ips, path):

        if not ips:
            message = _('Failed to build export location due to missing IP.')
            raise exception.InvalidInput(reason=message)

        if not path:
            message = _('Failed to build export location due to missing path.')
            raise exception.InvalidInput(reason=message)

        share_proto = HPE3ParMediator.ensure_supported_protocol(protocol)
        if share_proto == 'nfs':
            return ['%s:%s' % (ip, path) for ip in ips]
        else:
            return [r'\\%s\%s' % (ip, path) for ip in ips]

    def get_provisioned_gb(self, fpg):
        total_mb = 0
        try:
            result = self._client.getfsquota(fpg=fpg)
        except Exception as e:
            result = {'message': str(e)}

        error_msg = result.get('message')
        if error_msg:
            message = (_('Error while getting fsquotas for FPG '
                         '%(fpg)s: %(msg)s') %
                       {'fpg': fpg, 'msg': error_msg})
            LOG.error(message)
            raise exception.ShareBackendException(msg=message)

        for fsquota in result['members']:
            total_mb += float(fsquota['hardBlock'])
        return total_mb / units.Ki

    def get_fpg_status(self, fpg):
        """Get capacity and capabilities for FPG."""

        try:
            result = self._client.getfpg(fpg)
        except Exception as e:
            msg = (_('Failed to get capacity for fpg %(fpg)s: %(e)s') %
                   {'fpg': fpg, 'e': str(e)})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        if result['total'] != 1:
            msg = (_('Failed to get capacity for fpg %s.') % fpg)
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        member = result['members'][0]
        total_capacity_gb = float(member['capacityKiB']) / units.Mi
        free_capacity_gb = float(member['availCapacityKiB']) / units.Mi

        volumes = member['vvs']
        if isinstance(volumes, list):
            volume = volumes[0]  # Use first name from list
        else:
            volume = volumes  # There is just a name

        self._wsapi_login()
        try:
            volume_info = self._client.getVolume(volume)
            volume_set = self._client.getVolumeSet(fpg)
        finally:
            self._wsapi_logout()

        provisioning_type = volume_info['provisioningType']
        if provisioning_type not in (THIN, FULL, DEDUPE):
            msg = (_('Unexpected provisioning type for FPG %(fpg)s: '
                     '%(ptype)s.') % {'fpg': fpg, 'ptype': provisioning_type})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        dedupe = provisioning_type == DEDUPE
        thin_provisioning = provisioning_type in (THIN, DEDUPE)

        flash_cache_policy = volume_set.get('flashCachePolicy', DISABLED)
        hpe3par_flash_cache = flash_cache_policy == ENABLED

        status = {
            'pool_name': fpg,
            'total_capacity_gb': total_capacity_gb,
            'free_capacity_gb': free_capacity_gb,
            'thin_provisioning': thin_provisioning,
            'dedupe': dedupe,
            'hpe3par_flash_cache': hpe3par_flash_cache,
            'hp3par_flash_cache': hpe3par_flash_cache,
        }

        if thin_provisioning:
            status['provisioned_capacity_gb'] = self.get_provisioned_gb(fpg)

        return status

    @staticmethod
    def ensure_supported_protocol(share_proto):
        protocol = share_proto.lower()
        if protocol == 'cifs':
            protocol = 'smb'
        if protocol not in ['smb', 'nfs']:
            message = (_('Invalid protocol. Expected nfs or smb. Got %s.') %
                       protocol)
            LOG.error(message)
            raise exception.InvalidShareAccess(reason=message)
        return protocol

    @staticmethod
    def other_protocol(share_proto):
        """Given 'nfs' or 'smb' (or equivalent) return the other one."""
        protocol = HPE3ParMediator.ensure_supported_protocol(share_proto)
        return 'nfs' if protocol == 'smb' else 'smb'

    @staticmethod
    def ensure_prefix(uid, protocol=None, readonly=False):
        if uid.startswith('osf-'):
            return uid

        if protocol:
            proto = '-%s' % HPE3ParMediator.ensure_supported_protocol(protocol)
        else:
            proto = ''

        if readonly:
            ro = '-ro'
        else:
            ro = ''

        # Format is osf[-ro]-{nfs|smb}-uid
        return 'osf%s%s-%s' % (proto, ro, uid)

    @staticmethod
    def _get_nfs_options(extra_specs, readonly):
        """Validate the NFS extra_specs and return the options to use."""

        nfs_options = extra_specs.get('hpe3par:nfs_options')
        if nfs_options is None:
            nfs_options = extra_specs.get('hp3par:nfs_options')
            if nfs_options:
                msg = ("hp3par:nfs_options is deprecated. Use "
                       "hpe3par:nfs_options instead.")
                LOG.warning(msg)

        if nfs_options:
            options = nfs_options.split(',')
        else:
            options = []

        # rw, ro, and (no)root_squash (in)secure options are not allowed in
        # extra_specs because they will be forcibly set below.
        # no_subtree_check and fsid are not allowed per 3PAR support.
        # Other strings will be allowed to be sent to the 3PAR which will do
        # further validation.
        options_not_allowed = ['ro', 'rw',
                               'no_root_squash', 'root_squash',
                               'secure', 'insecure',
                               'no_subtree_check', 'fsid']

        invalid_options = [
            option for option in options if option in options_not_allowed
        ]

        if invalid_options:
            raise exception.InvalidInput(_('Invalid hp3par:nfs_options or '
                                           'hpe3par:nfs_options in '
                                           'extra-specs. The following '
                                           'options are not allowed: %s') %
                                         invalid_options)

        options.append('ro' if readonly else 'rw')
        options.append('no_root_squash')
        options.append('insecure')

        return ','.join(options)

    def _build_createfshare_kwargs(self, protocol, fpg, fstore, readonly,
                                   sharedir, extra_specs, comment,
                                   client_ip=None):
        createfshare_kwargs = dict(fpg=fpg,
                                   fstore=fstore,
                                   sharedir=sharedir,
                                   comment=comment)

        if 'hp3par_flash_cache' in extra_specs:
            msg = ("hp3par_flash_cache is deprecated. Use "
                   "hpe3par_flash_cache instead.")
            LOG.warning(msg)

        if protocol == 'nfs':
            if client_ip:
                createfshare_kwargs['clientip'] = client_ip
            else:
                # New NFS shares needs seed IP to prevent "all" access.
                # Readonly and readwrite NFS shares client IPs cannot overlap.
                if readonly:
                    createfshare_kwargs['clientip'] = LOCAL_IP_RO
                else:
                    createfshare_kwargs['clientip'] = LOCAL_IP
            options = self._get_nfs_options(extra_specs, readonly)
            createfshare_kwargs['options'] = options
        else:

            # To keep the original (Kilo, Liberty) behavior where CIFS IP
            # access rules were required in addition to user rules enable
            # this to use a seed IP instead of the default (all allowed).
            if self.hpe3par_require_cifs_ip:
                if client_ip:
                    createfshare_kwargs['allowip'] = client_ip
                else:
                    createfshare_kwargs['allowip'] = LOCAL_IP

            smb_opts = (ACCESS_BASED_ENUM, CONTINUOUS_AVAIL, CACHE)

            for smb_opt in smb_opts:
                opt_value = extra_specs.get('hpe3par:smb_%s' % smb_opt)
                if opt_value is None:
                    opt_value = extra_specs.get('hp3par:smb_%s' % smb_opt)
                    if opt_value:
                        msg = ("hp3par:smb_* is deprecated. Use "
                               "hpe3par:smb_* instead.")
                        LOG.warning(msg)

                if opt_value:
                    opt_key = SMB_EXTRA_SPECS_MAP[smb_opt]
                    createfshare_kwargs[opt_key] = opt_value
        return createfshare_kwargs

    def _update_capacity_quotas(self, fstore, new_size, old_size, fpg, vfs):

        @utils.synchronized('hpe3par-update-quota-' + fstore)
        def _sync_update_capacity_quotas(fstore, new_size, old_size, fpg, vfs):
            """Update 3PAR quotas and return setfsquota output."""

            if self.hpe3par_fstore_per_share:
                hcapacity = str(new_size * units.Ki)
                scapacity = hcapacity
            else:
                hard_size_mb = (new_size - old_size) * units.Ki
                soft_size_mb = hard_size_mb
                result = self._client.getfsquota(
                    fpg=fpg, vfs=vfs, fstore=fstore)
                LOG.debug("getfsquota result=%s", result)
                quotas = result['members']
                if len(quotas) == 1:
                    hard_size_mb += int(quotas[0].get('hardBlock', '0'))
                    soft_size_mb += int(quotas[0].get('softBlock', '0'))
                hcapacity = str(hard_size_mb)
                scapacity = str(soft_size_mb)

            return self._client.setfsquota(vfs,
                                           fpg=fpg,
                                           fstore=fstore,
                                           scapacity=scapacity,
                                           hcapacity=hcapacity)

        try:
            result = _sync_update_capacity_quotas(
                fstore, new_size, old_size, fpg, vfs)
            LOG.debug("setfsquota result=%s", result)
        except Exception as e:
            msg = (_('Failed to update capacity quota '
                     '%(size)s on %(fstore)s with exception: %(e)s') %
                   {'size': new_size - old_size,
                    'fstore': fstore,
                    'e': str(e)})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        # Non-empty result is an error message returned from the 3PAR
        if result:
            msg = (_('Failed to update capacity quota '
                     '%(size)s on %(fstore)s with error: %(error)s') %
                   {'size': new_size - old_size,
                    'fstore': fstore,
                    'error': result})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

    def _create_share(self, project_id, share_id, protocol, extra_specs,
                      fpg, vfs, fstore, sharedir, readonly, size, comment,
                      client_ip=None):
        share_name = self.ensure_prefix(share_id, readonly=readonly)

        if not (sharedir or self.hpe3par_fstore_per_share):
            sharedir = share_name

        if fstore:
            use_existing_fstore = True
        else:
            use_existing_fstore = False
            if self.hpe3par_fstore_per_share:
                # Do not use -ro in the fstore name.
                fstore = self.ensure_prefix(share_id, readonly=False)
            else:
                fstore = self.ensure_prefix(project_id, protocol)

        createfshare_kwargs = self._build_createfshare_kwargs(
            protocol,
            fpg,
            fstore,
            readonly,
            sharedir,
            extra_specs,
            comment,
            client_ip=client_ip)

        if not use_existing_fstore:

            try:
                result = self._client.createfstore(
                    vfs, fstore, fpg=fpg,
                    comment=comment)
                LOG.debug("createfstore result=%s", result)
            except Exception as e:
                msg = (_('Failed to create fstore %(fstore)s: %(e)s') %
                       {'fstore': fstore, 'e': str(e)})
                LOG.exception(msg)
                raise exception.ShareBackendException(msg=msg)

            if size:
                self._update_capacity_quotas(fstore, size, 0, fpg, vfs)

        try:

            if readonly and protocol == 'nfs':
                # For NFS, RO is a 2nd 3PAR share pointing to same sharedir
                share_name = self.ensure_prefix(share_id, readonly=readonly)

            result = self._client.createfshare(protocol,
                                               vfs,
                                               share_name,
                                               **createfshare_kwargs)

            LOG.debug("createfshare result=%s", result)

        except Exception as e:
            msg = (_('Failed to create share %(share_name)s: %(e)s') %
                   {'share_name': share_name, 'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        try:
            result = self._client.getfshare(
                protocol, share_name,
                fpg=fpg, vfs=vfs, fstore=fstore)
            LOG.debug("getfshare result=%s", result)

        except Exception as e:
            msg = (_('Failed to get fshare %(share_name)s after creating it: '
                     '%(e)s') % {'share_name': share_name,
                                 'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        if result['total'] != 1:
            msg = (_('Failed to get fshare %(share_name)s after creating it. '
                     'Expected to get 1 fshare.  Got %(total)s.') %
                   {'share_name': share_name, 'total': result['total']})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)
        return result['members'][0]

    def create_share(self, project_id, share_id, share_proto, extra_specs,
                     fpg, vfs,
                     fstore=None, sharedir=None, readonly=False, size=None,
                     comment=OPEN_STACK_MANILA,
                     client_ip=None):
        """Create the share and return its path.

        This method can create a share when called by the driver or when
        called locally from create_share_from_snapshot().  The optional
        parameters allow re-use.

        :param project_id: The tenant ID.
        :param share_id: The share-id with or without osf- prefix.
        :param share_proto: The protocol (to map to smb or nfs)
        :param extra_specs: The share type extra-specs
        :param fpg: The file provisioning group
        :param vfs:  The virtual file system
        :param fstore:  (optional) The file store.  When provided, an existing
        file store is used.  Otherwise one is created.
        :param sharedir: (optional) Share directory.
        :param readonly: (optional) Create share as read-only.
        :param size: (optional) Size limit for file store if creating one.
        :param comment: (optional) Comment to set on the share.
        :param client_ip: (optional) IP address to give access to.
        :return: share path string
        """

        protocol = self.ensure_supported_protocol(share_proto)
        share = self._create_share(project_id,
                                   share_id,
                                   protocol,
                                   extra_specs,
                                   fpg,
                                   vfs,
                                   fstore,
                                   sharedir,
                                   readonly,
                                   size,
                                   comment,
                                   client_ip=client_ip)

        if protocol == 'nfs':
            return share['sharePath']
        else:
            return share['shareName']

    def create_share_from_snapshot(self, share_id, share_proto, extra_specs,
                                   orig_project_id, orig_share_id,
                                   snapshot_id, fpg, vfs, ips,
                                   size=None,
                                   comment=OPEN_STACK_MANILA):

        protocol = self.ensure_supported_protocol(share_proto)
        snapshot_tag = self.ensure_prefix(snapshot_id)
        orig_share_name = self.ensure_prefix(orig_share_id)

        snapshot = self._find_fsnap(orig_project_id,
                                    orig_share_name,
                                    protocol,
                                    snapshot_tag,
                                    fpg,
                                    vfs)

        if not snapshot:
            msg = (_('Failed to create share from snapshot for '
                     'FPG/VFS/tag %(fpg)s/%(vfs)s/%(tag)s. '
                     'Snapshot not found.') %
                   {
                       'fpg': fpg,
                       'vfs': vfs,
                       'tag': snapshot_tag})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        fstore = snapshot['fstoreName']
        if fstore == orig_share_name:
            # No subdir for original share created with fstore_per_share
            sharedir = '.snapshot/%s' % snapshot['snapName']
        else:
            sharedir = '.snapshot/%s/%s' % (snapshot['snapName'],
                                            orig_share_name)

        if protocol == "smb" and (not self.hpe3par_cifs_admin_access_username
           or not self.hpe3par_cifs_admin_access_password):
            LOG.warning("hpe3par_cifs_admin_access_username and "
                        "hpe3par_cifs_admin_access_password must be "
                        "provided in order for CIFS shares created from "
                        "snapshots to be writable.")
            return self.create_share(
                orig_project_id,
                share_id,
                protocol,
                extra_specs,
                fpg,
                vfs,
                fstore=fstore,
                sharedir=sharedir,
                readonly=True,
                comment=comment,
            )

        # Export the snapshot as read-only to copy from.
        temp = ' '.join((comment, TMP_RO_SNAP_EXPORT))
        source_path = self.create_share(
            orig_project_id,
            share_id,
            protocol,
            extra_specs,
            fpg,
            vfs,
            fstore=fstore,
            sharedir=sharedir,
            readonly=True,
            comment=temp,
            client_ip=self.my_ip
        )

        try:
            share_name = self.ensure_prefix(share_id)
            dest_path = self.create_share(
                orig_project_id,
                share_id,
                protocol,
                extra_specs,
                fpg,
                vfs,
                fstore=fstore,
                readonly=False,
                size=size,
                comment=comment,
                client_ip=','.join((self.my_ip, LOCAL_IP))
            )

            try:
                if protocol == 'smb':
                    self._grant_admin_smb_access(
                        protocol, fpg, vfs, fstore, comment, share=share_name)

                    ro_share_name = self.ensure_prefix(share_id, readonly=True)
                    self._grant_admin_smb_access(
                        protocol, fpg, vfs, fstore, temp, share=ro_share_name)

                source_locations = self.build_export_locations(
                    protocol, ips, source_path)
                dest_locations = self.build_export_locations(
                    protocol, ips, dest_path)

                self._copy_share_data(
                    share_id, source_locations[0], dest_locations[0], protocol)

                # Revoke the admin access that was needed to copy to the dest.
                if protocol == 'nfs':
                    self._change_access(DENY,
                                        orig_project_id,
                                        share_id,
                                        protocol,
                                        'ip',
                                        self.my_ip,
                                        'rw',
                                        fpg,
                                        vfs)
                else:
                    self._revoke_admin_smb_access(
                        protocol, fpg, vfs, fstore, comment)

            except Exception as e:
                msg = ('Exception during mount and copy from RO snapshot '
                       'to RW share: %s')
                LOG.error(msg, e)
                self._delete_share(share_name, protocol, fpg, vfs, fstore)
                raise

        finally:
            self._delete_ro_share(
                orig_project_id, share_id, protocol, fpg, vfs, fstore)

        return dest_path

    def _copy_share_data(self, dest_id, source_location, dest_location,
                         protocol):

        mount_location = "%s%s" % (self.hpe3par_share_mount_path, dest_id)
        source_share_dir = '/'.join((mount_location, "source_snap"))
        dest_share_dir = '/'.join((mount_location, "dest_share"))

        dirs_to_remove = []
        dirs_to_unmount = []
        try:
            utils.execute('mkdir', '-p', source_share_dir, run_as_root=True)
            dirs_to_remove.append(source_share_dir)
            self._mount_share(protocol, source_location, source_share_dir)
            dirs_to_unmount.append(source_share_dir)

            utils.execute('mkdir', dest_share_dir, run_as_root=True)
            dirs_to_remove.append(dest_share_dir)
            self._mount_share(protocol, dest_location, dest_share_dir)
            dirs_to_unmount.append(dest_share_dir)

            self._copy_data(source_share_dir, dest_share_dir)
        finally:
            for d in dirs_to_unmount:
                self._unmount_share(d)

            if dirs_to_remove:
                dirs_to_remove.append(mount_location)
                utils.execute('rmdir', *dirs_to_remove, run_as_root=True)

    def _copy_data(self, source_share_dir, dest_share_dir):

        err_msg = None
        err_data = None
        try:
            copy = data_utils.Copy(source_share_dir, dest_share_dir, '')
            copy.run()
            progress = copy.get_progress()['total_progress']
            if progress != 100:
                err_msg = _("Failed to copy data, reason: "
                            "Total progress %d != 100.")
                err_data = progress
        except Exception as err:
            err_msg = _("Failed to copy data, reason: %s.")
            err_data = str(err)

        if err_msg:
            raise exception.ShareBackendException(msg=err_msg % err_data)

    def _delete_share(self, share_name, protocol, fpg, vfs, fstore):
        try:
            self._client.removefshare(
                protocol, vfs, share_name, fpg=fpg, fstore=fstore)

        except Exception as e:
            msg = (_('Failed to remove share %(share_name)s: %(e)s') %
                   {'share_name': share_name, 'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

    def _delete_ro_share(self, project_id, share_id, protocol,
                         fpg, vfs, fstore):
        share_name_ro = self.ensure_prefix(share_id, readonly=True)
        if not fstore:
            fstore = self._find_fstore(project_id,
                                       share_name_ro,
                                       protocol,
                                       fpg,
                                       vfs,
                                       allow_cross_protocol=True)
        if fstore:
            self._delete_share(share_name_ro, protocol, fpg, vfs, fstore)
        return fstore

    def delete_share(self, project_id, share_id, share_size, share_proto,
                     fpg, vfs, share_ip):

        protocol = self.ensure_supported_protocol(share_proto)
        share_name = self.ensure_prefix(share_id)
        fstore = self._find_fstore(project_id,
                                   share_name,
                                   protocol,
                                   fpg,
                                   vfs,
                                   allow_cross_protocol=True)

        removed_writable = False
        if fstore:
            self._delete_share(share_name, protocol, fpg, vfs, fstore)
            removed_writable = True

        # Try to delete the read-only twin share, too.
        fstore = self._delete_ro_share(
            project_id, share_id, protocol, fpg, vfs, fstore)

        if fstore == share_name:
            try:
                self._client.removefstore(vfs, fstore, fpg=fpg)
            except Exception as e:
                msg = (_('Failed to remove fstore %(fstore)s: %(e)s') %
                       {'fstore': fstore, 'e': str(e)})
                LOG.exception(msg)
                raise exception.ShareBackendException(msg=msg)

        elif removed_writable:
            try:
                # Attempt to remove file tree on delete when using nested
                # shares. If the file tree cannot be removed for whatever
                # reason, we will not treat this as an error_deleting
                # issue. We will allow the delete to continue as requested.
                self._delete_file_tree(
                    share_name, protocol, fpg, vfs, fstore, share_ip)
                # reduce the fsquota by share size when a tree is deleted.
                self._update_capacity_quotas(
                    fstore, 0, share_size, fpg, vfs)
            except Exception as e:
                msg = ('Exception during cleanup of deleted '
                       'share %(share)s in filestore %(fstore)s: %(e)s')
                data = {
                    'fstore': fstore,
                    'share': share_name,
                    'e': str(e),
                }
                LOG.warning(msg, data)

    def _delete_file_tree(self, share_name, protocol, fpg, vfs, fstore,
                          share_ip):
        # If the share protocol is CIFS, we need to make sure the admin
        # provided the proper config values. If they have not, we can simply
        # return out and log a warning.
        if protocol == "smb" and (not self.hpe3par_cifs_admin_access_username
           or not self.hpe3par_cifs_admin_access_password):
            LOG.warning("hpe3par_cifs_admin_access_username and "
                        "hpe3par_cifs_admin_access_password must be "
                        "provided in order for the file tree to be "
                        "properly deleted.")
            return

        mount_location = "%s%s" % (self.hpe3par_share_mount_path, share_name)
        share_dir = mount_location + "/%s" % share_name

        # Create the super share.
        self._create_super_share(protocol, fpg, vfs, fstore)

        # Create the mount directory.
        self._create_mount_directory(mount_location)

        # Mount the super share.
        self._mount_super_share(protocol, mount_location, fpg, vfs, fstore,
                                share_ip)

        # Delete the share from the super share.
        self._delete_share_directory(share_dir)

        # Unmount the super share.
        self._unmount_share(mount_location)

        # Delete the mount directory.
        self._delete_share_directory(mount_location)

    def _grant_admin_smb_access(self, protocol, fpg, vfs, fstore, comment,
                                share=SUPER_SHARE):
        user = '+%s:fullcontrol' % self.hpe3par_cifs_admin_access_username
        setfshare_kwargs = {
            'fpg': fpg,
            'fstore': fstore,
            'comment': comment,
            'allowperm': user,
        }
        try:
            self._client.setfshare(
                protocol, vfs, share, **setfshare_kwargs)
        except Exception as err:
            raise exception.ShareBackendException(
                msg=_("There was an error adding permissions: %s") % err)

    def _revoke_admin_smb_access(self, protocol, fpg, vfs, fstore, comment,
                                 share=SUPER_SHARE):
        user = '-%s:fullcontrol' % self.hpe3par_cifs_admin_access_username
        setfshare_kwargs = {
            'fpg': fpg,
            'fstore': fstore,
            'comment': comment,
            'allowperm': user,
        }
        try:
            self._client.setfshare(
                protocol, vfs, share, **setfshare_kwargs)
        except Exception as err:
            raise exception.ShareBackendException(
                msg=_("There was an error revoking permissions: %s") % err)

    def _create_super_share(self, protocol, fpg, vfs, fstore, readonly=False):
        sharedir = ''
        extra_specs = {}
        comment = 'OpenStack super share used to delete nested shares.'
        createfshare_kwargs = self._build_createfshare_kwargs(protocol,
                                                              fpg,
                                                              fstore,
                                                              readonly,
                                                              sharedir,
                                                              extra_specs,
                                                              comment)

        # If the share is NFS, we need to give the host access to the share in
        # order to properly mount it.
        if protocol == 'nfs':
            createfshare_kwargs['clientip'] = self.my_ip
        else:
            createfshare_kwargs['allowip'] = self.my_ip

        try:
            result = self._client.createfshare(protocol,
                                               vfs,
                                               SUPER_SHARE,
                                               **createfshare_kwargs)
            LOG.debug("createfshare for %(name)s, result=%(result)s",
                      {'name': SUPER_SHARE, 'result': result})
        except Exception as e:
            msg = (_('Failed to create share %(share_name)s: %(e)s'),
                   {'share_name': SUPER_SHARE, 'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        # If the share is CIFS, we need to grant access to the specified admin.
        if protocol == 'smb':
            self._grant_admin_smb_access(protocol, fpg, vfs, fstore, comment)

    def _create_mount_directory(self, mount_location):
        try:
            utils.execute('mkdir', mount_location, run_as_root=True)
        except Exception as err:
            message = ("There was an error creating mount directory: "
                       "%s. The nested file tree will not be deleted.",
                       str(err))
            LOG.warning(message)

    def _mount_share(self, protocol, export_location, mount_dir):
        if protocol == 'nfs':
            cmd = ('mount', '-t', 'nfs', export_location, mount_dir)
            utils.execute(*cmd, run_as_root=True)
        else:
            export_location = export_location.replace('\\', '/')
            cred = ('username=' + self.hpe3par_cifs_admin_access_username +
                    ',password=' +
                    self.hpe3par_cifs_admin_access_password +
                    ',domain=' + self.hpe3par_cifs_admin_access_domain)
            cmd = ('mount', '-t', 'cifs', export_location, mount_dir,
                   '-o', cred)
            utils.execute(*cmd, run_as_root=True)

    def _mount_super_share(self, protocol, mount_dir, fpg, vfs, fstore,
                           share_ip):
        try:
            mount_location = self._generate_mount_path(
                protocol, fpg, vfs, fstore, share_ip)
            self._mount_share(protocol, mount_location, mount_dir)
        except Exception as err:
            message = ("There was an error mounting the super share: "
                       "%s. The nested file tree will not be deleted.",
                       str(err))
            LOG.warning(message)

    def _unmount_share(self, mount_location):
        try:
            utils.execute('umount', mount_location, run_as_root=True)
        except Exception as err:
            message = ("There was an error unmounting the share at "
                       "%(mount_location)s: %(error)s")
            msg_data = {
                'mount_location': mount_location,
                'error': str(err),
            }
            LOG.warning(message, msg_data)

    def _delete_share_directory(self, directory):
        try:
            utils.execute('rm', '-rf', directory, run_as_root=True)
        except Exception as err:
            message = ("There was an error removing the share: "
                       "%s. The nested file tree will not be deleted.",
                       str(err))
            LOG.warning(message)

    def _generate_mount_path(self, protocol, fpg, vfs, fstore, share_ip):
        path = None
        if protocol == 'nfs':
            path = (("%(share_ip)s:/%(fpg)s/%(vfs)s/%(fstore)s/") %
                    {'share_ip': share_ip,
                     'fpg': fpg,
                     'vfs': vfs,
                     'fstore': fstore})
        else:
            path = (("//%(share_ip)s/%(share_name)s/") %
                    {'share_ip': share_ip,
                     'share_name': SUPER_SHARE})
        return path

    def get_vfs(self, fpg, vfs=None):
        """Get the VFS or raise an exception."""

        try:
            result = self._client.getvfs(fpg=fpg, vfs=vfs)
        except Exception as e:
            msg = (_('Exception during getvfs %(vfs)s: %(e)s') %
                   {'vfs': vfs, 'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        if result['total'] != 1:
            error_msg = result.get('message')
            if error_msg:
                message = (_('Error while validating FPG/VFS '
                             '(%(fpg)s/%(vfs)s): %(msg)s') %
                           {'fpg': fpg, 'vfs': vfs, 'msg': error_msg})
                LOG.error(message)
                raise exception.ShareBackendException(msg=message)
            else:
                message = (_('Error while validating FPG/VFS '
                             '(%(fpg)s/%(vfs)s): Expected 1, '
                             'got %(total)s.') %
                           {'fpg': fpg, 'vfs': vfs,
                            'total': result['total']})

                LOG.error(message)
                raise exception.ShareBackendException(msg=message)

        value = result['members'][0]
        if isinstance(value['vfsip'], dict):
            # This is for 3parclient returning only one VFS entry
            LOG.debug("3parclient version up to 4.2.1 is in use. Client "
                      "upgrade may be needed if using a VFS with multiple "
                      "IP addresses.")
            value['vfsip']['address'] = [value['vfsip']['address']]
        else:
            # This is for 3parclient returning list of VFS entries
            # Format get_vfs ret value to combine all IP addresses
            discovered_vfs_ips = []
            for vfs_entry in value['vfsip']:
                if vfs_entry['address']:
                    discovered_vfs_ips.append(vfs_entry['address'])
            value['vfsip'] = value['vfsip'][0]
            value['vfsip']['address'] = discovered_vfs_ips
        return value

    @staticmethod
    def _is_share_from_snapshot(fshare):

        path = fshare.get('shareDir')
        if path:
            return '.snapshot' in path.split('/')

        path = fshare.get('sharePath')
        return path and '.snapshot' in path.split('/')

    def create_snapshot(self, orig_project_id, orig_share_id, orig_share_proto,
                        snapshot_id, fpg, vfs):
        """Creates a snapshot of a share."""

        fshare = self._find_fshare(orig_project_id,
                                   orig_share_id,
                                   orig_share_proto,
                                   fpg,
                                   vfs)

        if not fshare:
            msg = (_('Failed to create snapshot for FPG/VFS/fshare '
                     '%(fpg)s/%(vfs)s/%(fshare)s: Failed to find fshare.') %
                   {'fpg': fpg, 'vfs': vfs, 'fshare': orig_share_id})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        if self._is_share_from_snapshot(fshare):
            msg = (_('Failed to create snapshot for FPG/VFS/fshare '
                     '%(fpg)s/%(vfs)s/%(fshare)s: Share is a read-only '
                     'share of an existing snapshot.') %
                   {'fpg': fpg, 'vfs': vfs, 'fshare': orig_share_id})
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

        fstore = fshare.get('fstoreName')
        snapshot_tag = self.ensure_prefix(snapshot_id)
        try:
            result = self._client.createfsnap(
                vfs, fstore, snapshot_tag, fpg=fpg)

            LOG.debug("createfsnap result=%s", result)

        except Exception as e:
            msg = (_('Failed to create snapshot for FPG/VFS/fstore '
                     '%(fpg)s/%(vfs)s/%(fstore)s: %(e)s') %
                   {'fpg': fpg, 'vfs': vfs, 'fstore': fstore,
                    'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

    def delete_snapshot(self, orig_project_id, orig_share_id, orig_proto,
                        snapshot_id, fpg, vfs):
        """Deletes a snapshot of a share."""

        snapshot_tag = self.ensure_prefix(snapshot_id)

        snapshot = self._find_fsnap(orig_project_id, orig_share_id, orig_proto,
                                    snapshot_tag, fpg, vfs)

        if not snapshot:
            return

        fstore = snapshot.get('fstoreName')

        for protocol in ('nfs', 'smb'):
            try:
                shares = self._client.getfshare(protocol,
                                                fpg=fpg,
                                                vfs=vfs,
                                                fstore=fstore)
            except Exception as e:
                msg = (_('Unexpected exception while getting share list. '
                         'Cannot delete snapshot without checking for '
                         'dependent shares first: %s') % str(e))
                LOG.exception(msg)
                raise exception.ShareBackendException(msg=msg)

            for share in shares['members']:
                if protocol == 'nfs':
                    path = share['sharePath'][1:].split('/')
                    dot_snapshot_index = 3
                else:
                    if share['shareDir']:
                        path = share['shareDir'].split('/')
                    else:
                        path = None
                    dot_snapshot_index = 0

                snapshot_index = dot_snapshot_index + 1
                if path and len(path) > snapshot_index:
                    if (path[dot_snapshot_index] == '.snapshot' and
                            path[snapshot_index].endswith(snapshot_tag)):
                        msg = (_('Cannot delete snapshot because it has a '
                                 'dependent share.'))
                        raise exception.Invalid(msg)

        snapname = snapshot['snapName']
        try:
            result = self._client.removefsnap(
                vfs, fstore, snapname=snapname, fpg=fpg)

            LOG.debug("removefsnap result=%s", result)

        except Exception as e:
            msg = (_('Failed to delete snapshot for FPG/VFS/fstore/snapshot '
                     '%(fpg)s/%(vfs)s/%(fstore)s/%(snapname)s: %(e)s') %
                   {
                       'fpg': fpg,
                       'vfs': vfs,
                       'fstore': fstore,
                       'snapname': snapname,
                       'e': str(e)})
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        # Try to reclaim the space
        try:
            self._client.startfsnapclean(fpg, reclaimStrategy='maxspeed')
        except Exception:
            # Remove already happened so only log this.
            LOG.exception('Unexpected exception calling startfsnapclean '
                          'for FPG %(fpg)s.', {'fpg': fpg})

    @staticmethod
    def _validate_access_type(protocol, access_type):

        if access_type not in ('ip', 'user'):
            msg = (_("Invalid access type.  Expected 'ip' or 'user'.  "
                     "Actual '%s'.") % access_type)
            LOG.error(msg)
            raise exception.InvalidInput(reason=msg)

        if protocol == 'nfs' and access_type != 'ip':
            msg = (_("Invalid NFS access type.  HPE 3PAR NFS supports 'ip'. "
                     "Actual '%s'.") % access_type)
            LOG.error(msg)
            raise exception.HPE3ParInvalid(err=msg)

        return protocol

    @staticmethod
    def _validate_access_level(protocol, access_type, access_level, fshare):

        readonly = access_level == 'ro'
        snapshot = HPE3ParMediator._is_share_from_snapshot(fshare)

        if snapshot and not readonly:
            reason = _('3PAR shares from snapshots require read-only access')
            LOG.error(reason)
            raise exception.InvalidShareAccess(reason=reason)

        if protocol == 'smb' and access_type == 'ip' and snapshot != readonly:
            msg = (_("Invalid CIFS access rule. HPE 3PAR optionally supports "
                     "IP access rules for CIFS shares, but they must be "
                     "read-only for shares from snapshots and read-write for "
                     "other shares. Use the required CIFS 'user' access rules "
                     "to refine access."))
            LOG.error(msg)
            raise exception.InvalidShareAccess(reason=msg)

    @staticmethod
    def ignore_benign_access_results(plus_or_minus, access_type, access_to,
                                     result):

        # TODO(markstur): Remove the next line when hpe3parclient is fixed.
        result = [x for x in result if x != '\r']

        if result:
            if plus_or_minus == DENY:
                if DOES_NOT_EXIST in result[0]:
                    return None
            else:
                if access_type == 'user':
                    if USER_ALREADY_EXISTS % access_to in result[0]:
                        return None
                elif IP_ALREADY_EXISTS % access_to in result[0]:
                    return None
        return result

    def _change_access(self, plus_or_minus, project_id, share_id, share_proto,
                       access_type, access_to, access_level,
                       fpg, vfs, extra_specs=None):
        """Allow or deny access to a share.

        Plus_or_minus character indicates add to allow list (+) or remove from
        allow list (-).
        """

        readonly = access_level == 'ro'
        protocol = self.ensure_supported_protocol(share_proto)

        try:
            self._validate_access_type(protocol, access_type)
        except Exception:
            if plus_or_minus == DENY:
                # Catch invalid rules for deny. Allow them to be deleted.
                return
            else:
                raise

        fshare = self._find_fshare(project_id,
                                   share_id,
                                   protocol,
                                   fpg,
                                   vfs,
                                   readonly=readonly)
        if not fshare:
            # Change access might apply to the share with the name that
            # does not match the access_level prefix.
            other_fshare = self._find_fshare(project_id,
                                             share_id,
                                             protocol,
                                             fpg,
                                             vfs,
                                             readonly=not readonly)
            if other_fshare:

                if plus_or_minus == DENY:
                    # Try to deny rule from 'other' share for SMB or legacy.
                    fshare = other_fshare

                elif self._is_share_from_snapshot(other_fshare):
                    # Found a share-from-snapshot from before
                    # "-ro" was added to the name. Use it.
                    fshare = other_fshare

                elif protocol == 'nfs':
                    # We don't have the RO|RW share we need, but the
                    # opposite one already exists. It is OK to create
                    # the one we need for ALLOW with NFS (not from snapshot).
                    fstore = other_fshare.get('fstoreName')
                    sharedir = other_fshare.get('shareDir')
                    comment = other_fshare.get('comment')

                    fshare = self._create_share(project_id,
                                                share_id,
                                                protocol,
                                                extra_specs,
                                                fpg,
                                                vfs,
                                                fstore=fstore,
                                                sharedir=sharedir,
                                                readonly=readonly,
                                                size=None,
                                                comment=comment)
                else:
                    # SMB only has one share for RO and RW. Try to use it.
                    fshare = other_fshare

            if not fshare:
                msg = _('Failed to change (%(change)s) access '
                        'to FPG/share %(fpg)s/%(share)s '
                        'for %(type)s %(to)s %(level)s): '
                        'Share does not exist on 3PAR.')
                msg_data = {
                    'change': plus_or_minus,
                    'fpg': fpg,
                    'share': share_id,
                    'type': access_type,
                    'to': access_to,
                    'level': access_level,
                }

                if plus_or_minus == DENY:
                    LOG.warning(msg, msg_data)
                    return
                else:
                    raise exception.HPE3ParInvalid(err=msg % msg_data)

        try:
            self._validate_access_level(
                protocol, access_type, access_level, fshare)
        except exception.InvalidShareAccess as e:
            if plus_or_minus == DENY:
                # Allow invalid access rules to be deleted.
                msg = _('Ignoring deny invalid access rule '
                        'for FPG/share %(fpg)s/%(share)s '
                        'for %(type)s %(to)s %(level)s): %(e)s')
                msg_data = {
                    'change': plus_or_minus,
                    'fpg': fpg,
                    'share': share_id,
                    'type': access_type,
                    'to': access_to,
                    'level': access_level,
                    'e': str(e),
                }
                LOG.info(msg, msg_data)
                return
            else:
                raise

        share_name = fshare.get('shareName')
        setfshare_kwargs = {
            'fpg': fpg,
            'fstore': fshare.get('fstoreName'),
            'comment': fshare.get('comment'),
        }

        if protocol == 'nfs':
            access_change = '%s%s' % (plus_or_minus, access_to)
            setfshare_kwargs['clientip'] = access_change

        elif protocol == 'smb':

            if access_type == 'ip':
                access_change = '%s%s' % (plus_or_minus, access_to)
                setfshare_kwargs['allowip'] = access_change

            else:
                access_str = 'read' if readonly else 'fullcontrol'
                perm = '%s%s:%s' % (plus_or_minus, access_to, access_str)
                setfshare_kwargs['allowperm'] = perm

        try:
            result = self._client.setfshare(
                protocol, vfs, share_name, **setfshare_kwargs)

            result = self.ignore_benign_access_results(
                plus_or_minus, access_type, access_to, result)

        except Exception as e:
            result = str(e)

        LOG.debug("setfshare result=%s", result)
        if result:
            msg = (_('Failed to change (%(change)s) access to FPG/share '
                     '%(fpg)s/%(share)s for %(type)s %(to)s %(level)s: '
                     '%(error)s') %
                   {'change': plus_or_minus,
                    'fpg': fpg,
                    'share': share_id,
                    'type': access_type,
                    'to': access_to,
                    'level': access_level,
                    'error': result})
            raise exception.ShareBackendException(msg=msg)

    def _find_fstore(self, project_id, share_id, share_proto, fpg, vfs,
                     allow_cross_protocol=False):

        share = self._find_fshare(project_id,
                                  share_id,
                                  share_proto,
                                  fpg,
                                  vfs,
                                  allow_cross_protocol=allow_cross_protocol)

        return share.get('fstoreName') if share else None

    def _find_fshare(self, project_id, share_id, share_proto, fpg, vfs,
                     allow_cross_protocol=False, readonly=False):

        share = self._find_fshare_with_proto(project_id,
                                             share_id,
                                             share_proto,
                                             fpg,
                                             vfs,
                                             readonly=readonly)

        if not share and allow_cross_protocol:
            other_proto = self.other_protocol(share_proto)
            share = self._find_fshare_with_proto(project_id,
                                                 share_id,
                                                 other_proto,
                                                 fpg,
                                                 vfs,
                                                 readonly=readonly)
        return share

    def _find_fshare_with_proto(self, project_id, share_id, share_proto,
                                fpg, vfs, readonly=False):

        protocol = self.ensure_supported_protocol(share_proto)
        share_name = self.ensure_prefix(share_id, readonly=readonly)

        project_fstore = self.ensure_prefix(project_id, share_proto)
        search_order = [
            {'fpg': fpg, 'vfs': vfs, 'fstore': project_fstore},
            {'fpg': fpg, 'vfs': vfs, 'fstore': share_name},
            {'fpg': fpg},
            {}
        ]

        try:
            for search_params in search_order:
                result = self._client.getfshare(protocol, share_name,
                                                **search_params)
                shares = result.get('members', [])
                if len(shares) == 1:
                    return shares[0]
        except Exception as e:
            msg = (_('Unexpected exception while getting share list: %s') %
                   str(e))
            raise exception.ShareBackendException(msg=msg)

    def _find_fsnap(self, project_id, share_id, orig_proto, snapshot_tag,
                    fpg, vfs):

        share_name = self.ensure_prefix(share_id)
        osf_project_id = self.ensure_prefix(project_id, orig_proto)
        pattern = '*_%s' % self.ensure_prefix(snapshot_tag)

        search_order = [
            {'pat': True, 'fpg': fpg, 'vfs': vfs, 'fstore': osf_project_id},
            {'pat': True, 'fpg': fpg, 'vfs': vfs, 'fstore': share_name},
            {'pat': True, 'fpg': fpg},
            {'pat': True},
        ]

        try:
            for search_params in search_order:
                result = self._client.getfsnap(pattern, **search_params)
                snapshots = result.get('members', [])
                if len(snapshots) == 1:
                    return snapshots[0]
        except Exception as e:
            msg = (_('Unexpected exception while getting snapshots: %s') %
                   str(e))
            raise exception.ShareBackendException(msg=msg)

    def update_access(self, project_id, share_id, share_proto, extra_specs,
                      access_rules, add_rules, delete_rules, fpg, vfs):
        """Update access to a share."""
        protocol = self.ensure_supported_protocol(share_proto)

        if not (delete_rules or add_rules):
            # We need to re add all the rules. Check with 3PAR on it's current
            # list and only add the deltas.
            share = self._find_fshare(project_id,
                                      share_id,
                                      share_proto,
                                      fpg,
                                      vfs)

            ref_users = []
            ro_ref_rules = []
            if protocol == 'nfs':
                ref_rules = share['clients']

                # Check for RO rules.
                ro_share = self._find_fshare(project_id,
                                             share_id,
                                             share_proto,
                                             fpg,
                                             vfs,
                                             readonly=True)
                if ro_share:
                    ro_ref_rules = ro_share['clients']
            else:
                ref_rules = [x[0] for x in share['allowPerm']]
                ref_users = ref_rules[:]
                # Get IP access as well
                ips = share['allowIP']
                if not isinstance(ips, list):
                    # If there is only one IP, the API returns a string
                    # rather than a list. We need to account for that.
                    ips = [ips]
                ref_rules += ips

            # Retrieve base rules.
            base_rules = []
            for rule in access_rules:
                base_rules.append(rule['access_to'])

            # Check if we need to remove any rules from 3PAR.
            for rule in ref_rules:
                if rule in ref_users:
                    rule_type = 'user'
                else:
                    rule_type = 'ip'

                if rule not in base_rules + [LOCAL_IP, LOCAL_IP_RO]:
                    self._change_access(DENY,
                                        project_id,
                                        share_id,
                                        share_proto,
                                        rule_type,
                                        rule,
                                        None,
                                        fpg,
                                        vfs)

            # Check to see if there are any RO rules to remove.
            for rule in ro_ref_rules:
                if rule not in base_rules + [LOCAL_IP, LOCAL_IP_RO]:
                    self._change_access(DENY,
                                        project_id,
                                        share_id,
                                        share_proto,
                                        rule_type,
                                        rule,
                                        'ro',
                                        fpg,
                                        vfs)

            # Check the rules we need to add.
            for rule in access_rules:
                if rule['access_to'] not in ref_rules and (
                   rule['access_to'] not in ro_ref_rules):
                    # Rule does not exist, we need to add it
                    self._change_access(ALLOW,
                                        project_id,
                                        share_id,
                                        share_proto,
                                        rule['access_type'],
                                        rule['access_to'],
                                        rule['access_level'],
                                        fpg,
                                        vfs,
                                        extra_specs=extra_specs)
        else:
            # We have deltas of the rules that need to be added and deleted.
            for rule in delete_rules:
                self._change_access(DENY,
                                    project_id,
                                    share_id,
                                    share_proto,
                                    rule['access_type'],
                                    rule['access_to'],
                                    rule['access_level'],
                                    fpg,
                                    vfs)
            for rule in add_rules:
                self._change_access(ALLOW,
                                    project_id,
                                    share_id,
                                    share_proto,
                                    rule['access_type'],
                                    rule['access_to'],
                                    rule['access_level'],
                                    fpg,
                                    vfs,
                                    extra_specs=extra_specs)

    def resize_share(self, project_id, share_id, share_proto,
                     new_size, old_size, fpg, vfs):
        """Extends or shrinks size of existing share."""

        share_name = self.ensure_prefix(share_id)
        fstore = self._find_fstore(project_id,
                                   share_name,
                                   share_proto,
                                   fpg,
                                   vfs,
                                   allow_cross_protocol=False)

        if not fstore:
            msg = (_('Cannot resize share because it was not found.'))
            raise exception.InvalidShare(reason=msg)

        self._update_capacity_quotas(fstore, new_size, old_size, fpg, vfs)

    def fsip_exists(self, fsip):
        """Try to get FSIP. Return True if it exists."""

        vfs = fsip['vfs']
        fpg = fsip['fspool']

        try:
            result = self._client.getfsip(vfs, fpg=fpg)
            LOG.debug("getfsip result: %s", result)
        except Exception:
            msg = (_('Failed to get FSIPs for FPG/VFS %(fspool)s/%(vfs)s.') %
                   fsip)
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        for member in result['members']:
            if all(item in member.items() for item in fsip.items()):
                return True

        return False

    def create_fsip(self, ip, subnet, vlantag, fpg, vfs):

        vlantag_str = str(vlantag) if vlantag else '0'

        # Try to create it. It's OK if it already exists.
        try:
            result = self._client.createfsip(ip,
                                             subnet,
                                             vfs,
                                             fpg=fpg,
                                             vlantag=vlantag_str)
            LOG.debug("createfsip result: %s", result)

        except Exception:
            msg = (_('Failed to create FSIP for %s') % ip)
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        # Verify that it really exists.
        fsip = {
            'fspool': fpg,
            'vfs': vfs,
            'address': ip,
            'prefixLen': subnet,
            'vlanTag': vlantag_str,
        }
        if not self.fsip_exists(fsip):
            msg = (_('Failed to get FSIP after creating it for '
                     'FPG/VFS/IP/subnet/VLAN '
                     '%(fspool)s/%(vfs)s/'
                     '%(address)s/%(prefixLen)s/%(vlanTag)s.') % fsip)
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)

    def remove_fsip(self, ip, fpg, vfs):

        if not (vfs and ip):
            # If there is no VFS and/or IP, then there is no FSIP to remove.
            return

        try:
            result = self._client.removefsip(vfs, ip, fpg=fpg)
            LOG.debug("removefsip result: %s", result)

        except Exception:
            msg = (_('Failed to remove FSIP %s') % ip)
            LOG.exception(msg)
            raise exception.ShareBackendException(msg=msg)

        # Verify that it really no longer exists.
        fsip = {
            'fspool': fpg,
            'vfs': vfs,
            'address': ip,
        }
        if self.fsip_exists(fsip):
            msg = (_('Failed to remove FSIP for FPG/VFS/IP '
                     '%(fspool)s/%(vfs)s/%(address)s.') % fsip)
            LOG.error(msg)
            raise exception.ShareBackendException(msg=msg)
